{"_id":"@anichkay/pwa-lib","_rev":"3-28d2365e875e8365321fda075e785e04","name":"@anichkay/pwa-lib","dist-tags":{"latest":"0.6.1"},"versions":{"0.3.0":{"name":"@anichkay/pwa-lib","version":"0.3.0","keywords":["pwa","progressive-web-app","cli","manifest","service-worker","icons"],"author":"","license":"MIT","_id":"@anichkay/pwa-lib@0.3.0","maintainers":[{"name":"anichkay","email":"anichkay@proton.me"}],"bin":{"pwa-lib":"dist/cli/index.js"},"dist":{"shasum":"420a6c33437cf6ceef8dc76f00ed2279a1c4c999","tarball":"https://registry.npmjs.org/@anichkay/pwa-lib/-/pwa-lib-0.3.0.tgz","fileCount":7,"integrity":"sha512-5xIQr97VC6elhtKl9xZOFAAg5Px0MPHKmCtgMLnxFswzEvqTIFT33pTG8TUDGkO58cPf0WEo7Zldy1LndA/mgA==","signatures":[{"sig":"MEYCIQDfBDDb9XQJyFPdR7hoarEEggRBuXvVr2K9BVtHDRgsuAIhAIYMgdFG+OzNRI+aOxp2B7+fzRsYYKDdWWx0rR2KSPGm","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":76614},"type":"module","exports":{".":{"types":"./dist/shared/index.d.ts","import":"./dist/shared/index.js"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js"},"./config":{"types":"./dist/shared/index.d.ts","import":"./dist/shared/index.js"}},"gitHead":"cc7e4d3280b0f00236ab73e257bc6f724e66d7f2","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"anichkay","email":"anichkay@proton.me"},"_npmVersion":"11.6.2","description":"Zero-config CLI library for full PWA management","directories":{},"_nodeVersion":"24.11.1","dependencies":{"defu":"^6.0.0","jiti":"^2.0.0","citty":"^0.1.0","sharp":"^0.33.0","globby":"^14.0.0","consola":"^3.0.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/pwa-lib_0.3.0_1770355828331_0.5133431938874828","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@anichkay/pwa-lib","version":"0.4.1","keywords":["pwa","progressive-web-app","cli","manifest","service-worker","icons"],"author":"","license":"MIT","_id":"@anichkay/pwa-lib@0.4.1","maintainers":[{"name":"anichkay","email":"anichkay@proton.me"}],"bin":{"pwa-lib":"dist/cli/index.js"},"dist":{"shasum":"f554cfd2c5cecd4cb58510e1e18152f766f92bb6","tarball":"https://registry.npmjs.org/@anichkay/pwa-lib/-/pwa-lib-0.4.1.tgz","fileCount":7,"integrity":"sha512-c3RN9tCuHU2pyzp5bXjIUwrcYr/ZjqHrrejUcxwvxxyElnv8mKpq1J33s0Ys2Fp0+/c5ZYW2lmC/daqFIWKBeA==","signatures":[{"sig":"MEUCIHWhihBkPYJJ3Q8N67fauM6NhHECL0QLKwZqHe09LrWeAiEAxWuSc9VEHlM0xbTdG6Mq8BfQUpkoMsjoin6hnKI2WCg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":90055},"type":"module","exports":{".":{"types":"./dist/shared/index.d.ts","import":"./dist/shared/index.js"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js"},"./config":{"types":"./dist/shared/index.d.ts","import":"./dist/shared/index.js"}},"gitHead":"8666bdc08c0358107d0bc6eda59e708e1aea8743","scripts":{"dev":"tsup --watch","build":"tsup"},"_npmUser":{"name":"anichkay","email":"anichkay@proton.me"},"_npmVersion":"11.6.2","description":"Zero-config CLI library for full PWA management","directories":{},"_nodeVersion":"24.11.1","dependencies":{"defu":"^6.0.0","jiti":"^2.0.0","citty":"^0.1.0","sharp":"^0.33.0","globby":"^14.0.0","consola":"^3.0.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/pwa-lib_0.4.1_1770393161189_0.7607202792445074","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"name":"@anichkay/pwa-lib","version":"0.6.1","description":"Zero-config CLI library for full PWA management","type":"module","author":"","license":"MIT","keywords":["pwa","progressive-web-app","cli","manifest","service-worker","icons"],"bin":{"pwa-lib":"dist/cli/index.js"},"exports":{".":{"types":"./dist/shared/index.d.ts","import":"./dist/shared/index.js"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js"},"./config":{"types":"./dist/shared/index.d.ts","import":"./dist/shared/index.js"}},"scripts":{"build":"tsup","dev":"tsup --watch"},"dependencies":{"sharp":"^0.33.0","citty":"^0.1.0","consola":"^3.0.0","jiti":"^2.0.0","defu":"^6.0.0","globby":"^14.0.0"},"devDependencies":{"tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0"},"gitHead":"f3a188a6dcc85d066fe2c3273bcf38aa000b6d77","_id":"@anichkay/pwa-lib@0.6.1","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-aQHqZzelGRCCETOKI0X7dGCeedoCYrffVIJK3VDFbxE95pqzq0QFpAV7VDn6/c4pawmo/qwUlYqF4Mjxrzk3+A==","shasum":"b6f6603ec4b5277757bcf9f1de2f27e22f30e412","tarball":"https://registry.npmjs.org/@anichkay/pwa-lib/-/pwa-lib-0.6.1.tgz","fileCount":7,"unpackedSize":97366,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCQ/6Aw9vK2ue5g6GXcVFzStxWa+SySPhq8l3r1LIH9FgIhAIDIGZ9TRRZeSPB9KnOEQpoew6aDwGmziMFYdHJb7CYV"}]},"_npmUser":{"name":"anichkay","email":"anichkay@proton.me"},"directories":{},"maintainers":[{"name":"anichkay","email":"anichkay@proton.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pwa-lib_0.6.1_1770394908377_0.0645927430089619"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-06T05:30:28.275Z","modified":"2026-02-06T16:21:48.624Z","0.3.0":"2026-02-06T05:30:28.481Z","0.4.1":"2026-02-06T15:52:41.355Z","0.6.1":"2026-02-06T16:21:48.519Z"},"license":"MIT","keywords":["pwa","progressive-web-app","cli","manifest","service-worker","icons"],"description":"Zero-config CLI library for full PWA management","maintainers":[{"name":"anichkay","email":"anichkay@proton.me"}],"readme":"# pwa-lib\n\nZero-config CLI-библиотека для полного управления PWA.\n\nУстановил пакет, запустил `pwa-lib generate` — получил готовые иконки всех размеров, `manifest.json` и `sw.js` с кэшированием. Конфиг нужен только если хочешь что-то изменить.\n\n## Что умеет\n\n- **Генерация иконок** — из одного PNG (>= 512x512) создаёт 12 стандартных размеров, maskable-иконку с safe zone, `favicon.ico` с тремя размерами внутри. Всего 14 файлов. Использует `sharp` — нативный и быстрый.\n- **Генерация manifest.json** — полный веб-манифест с иконками, цветами, display mode, orientation, scope. Автоматически берёт `name` и `description` из `package.json`.\n- **Генерация Service Worker** — готовый `sw.js` с 5 стратегиями кэширования, роутингом по URL-паттернам, precache, push-уведомлениями, управлением жизненным циклом. Без Workbox, без runtime-зависимостей.\n- **Push-уведомления** — в SW генерируется обработчик `push` и `notificationclick`. На клиенте — `createPushClient()` делает всё за один вызов: подписка, VAPID, отправка на сервер. Конфиг подтягивается из `pwa.config.ts` автоматически.\n- **Watch mode** — `pwa-lib dev` следит за конфигом и `public/`, перегенерирует SW при изменениях.\n- **Клиентская библиотека** — `pwa-lib/client` экспортирует `registerSW()` и `notifications` API. Pure ESM, zero dependencies, tree-shakeable.\n- **TypeScript** — полная типизация конфига, клиентского API и всех экспортов. `defineConfig()` даёт автокомплит в IDE.\n- **Автодетект** — иконка находится автоматически по 10 стандартным путям. `name`/`description` берутся из `package.json`. Дефолтные стратегии кэширования покрывают типичные сценарии.\n\n## Установка\n\n```bash\nnpm install pwa-lib\n```\n\n## Быстрый старт\n\n```bash\n# Вариант 1: всё на дефолтах, без конфига\nnpx pwa-lib generate\n\n# Вариант 2: создать конфиг, настроить, сгенерировать\nnpx pwa-lib init\n# отредактировать pwa.config.ts\nnpx pwa-lib generate\n```\n\nРезультат:\n```\npublic/\n├── icons/\n│   ├── icon-16.png\n│   ├── icon-32.png\n│   ├── icon-48.png\n│   ├── icon-72.png\n│   ├── icon-96.png\n│   ├── icon-128.png\n│   ├── icon-144.png\n│   ├── icon-152.png\n│   ├── icon-192.png\n│   ├── icon-384.png\n│   ├── icon-512.png\n│   ├── icon-512-maskable.png\n│   ├── apple-touch-icon.png\n│   └── favicon.ico\n├── manifest.json\n├── sw.js\n└── pwa-push.json          # если настроены serverUrl/appId/apiKey\n```\n\n---\n\n## Конфигурация\n\n### Философия\n\n**Без конфига работает всё.** Конфиг нужен только когда хочешь изменить дефолтное поведение. Каждое поле опционально. Можно указать только то, что отличается от дефолтов — остальное подставится автоматически.\n\n### Создание конфига\n\n```bash\npwa-lib init\n```\n\nСоздаёт `pwa.config.ts` с закомментированными примерами. Или создай файл вручную.\n\n### Форматы конфига\n\nCLI ищет конфиг в таком порядке:\n\n1. `pwa.config.ts` — TypeScript (рекомендуется, даёт автокомплит)\n2. `pwa.config.js` — ESM JavaScript\n3. `pwa.config.mjs` — ESM JavaScript (явный)\n\nTypeScript-конфиги загружаются через [jiti](https://github.com/unjs/jiti) — не нужен `ts-node`, `tsx` или другой рантайм. Просто пиши `.ts`.\n\n### Типизированный конфиг\n\nДля полного автокомплита в IDE используй `defineConfig()`:\n\n```ts\nimport { defineConfig } from 'pwa-lib/config'\n\nexport default defineConfig({\n  // автокомплит работает здесь\n})\n```\n\nИли без импорта (работает без установленного пакета, например сразу после `pwa-lib init`):\n\n```ts\n/** @type {import('pwa-lib').PwaConfig} */\nexport default {\n  // конфиг\n}\n```\n\n### Приоритет значений\n\nКонфиг мержится через `defu` (глубокий merge). Приоритет:\n\n1. **Твой `pwa.config.ts`** — наивысший приоритет\n2. **`package.json`** — `name` и `description` подставляются в `manifest.name`, `manifest.short_name`, `manifest.description`\n3. **Встроенные дефолты** — всё остальное\n\nЭто значит: если в `package.json` написано `\"name\": \"my-app\"`, то `manifest.name` будет `\"my-app\"` без какого-либо конфига. Но если в `pwa.config.ts` указать `manifest.name` — оно перебьёт значение из `package.json`.\n\n### Полный конфиг со всеми опциями\n\n```ts\nimport { defineConfig } from 'pwa-lib/config'\n\nexport default defineConfig({\n  // ─── Иконка ────────────────────────────────────────────\n  // Путь к исходному изображению для генерации всех размеров.\n  // Требования: PNG, минимум 512x512.\n  // Если не указан — автопоиск (см. ниже).\n  icon: './src/assets/icon.png',\n\n  // ─── Manifest ──────────────────────────────────────────\n  // Все поля Web App Manifest.\n  // name и description берутся из package.json если не указаны.\n  manifest: {\n    // Полное имя приложения. Показывается при установке, в списке приложений.\n    // Дефолт: package.json → name, или 'My PWA'\n    name: 'My Awesome App',\n\n    // Короткое имя. Показывается под иконкой на рабочем столе.\n    // Рекомендуется <= 12 символов.\n    // Дефолт: package.json → name, или 'PWA'\n    short_name: 'App',\n\n    // Описание приложения. Показывается в магазинах приложений.\n    // Дефолт: package.json → description, или ''\n    description: 'The best progressive web app',\n\n    // Цвет темы. Влияет на цвет адресной строки и системного UI.\n    // Дефолт: '#ffffff'\n    theme_color: '#4285f4',\n\n    // Цвет фона splash screen при запуске.\n    // Дефолт: '#ffffff'\n    background_color: '#ffffff',\n\n    // Режим отображения:\n    // - 'standalone' — как нативное приложение, без браузерного UI (рекомендуется)\n    // - 'fullscreen' — на весь экран, без status bar\n    // - 'minimal-ui' — с минимальной навигацией браузера\n    // - 'browser' — обычная вкладка браузера\n    // Дефолт: 'standalone'\n    display: 'standalone',\n\n    // URL, который открывается при запуске приложения.\n    // Дефолт: '/'\n    start_url: '/',\n\n    // Scope ограничивает навигацию приложения. URL за пределами scope\n    // открываются в обычном браузере.\n    // Дефолт: '/'\n    scope: '/',\n\n    // Язык приложения (BCP 47 tag).\n    // Дефолт: 'en'\n    lang: 'ru',\n\n    // Предпочтительная ориентация экрана:\n    // - 'any' — любая\n    // - 'natural' — по умолчанию для устройства\n    // - 'landscape' — горизонтальная\n    // - 'portrait' — вертикальная\n    // Дефолт: 'any'\n    orientation: 'portrait',\n  },\n\n  // ─── Service Worker ────────────────────────────────────\n  sw: {\n    // Куда записать сгенерированный sw.js.\n    // Дефолт: './public/sw.js'\n    output: './public/sw.js',\n\n    // Glob-паттерны файлов для precache (предзагрузка при установке SW).\n    // Все совпавшие файлы будут закэшированы при первом визите.\n    // Дефолт: [] (ничего не предзагружается)\n    precache: [\n      './public/**/*.html',\n      './public/**/*.css',\n      './public/**/*.js',\n    ],\n\n    // Стратегии кэширования по маршрутам.\n    // Порядок имеет значение — первый совпавший маршрут выигрывает.\n    // Дефолт: см. \"Дефолтные маршруты\" ниже.\n    routes: [\n      // API-запросы: сначала сеть, при ошибке — кэш\n      {\n        match: '/api/**',\n        strategy: 'NetworkFirst',\n        cache: 'api-cache',\n        maxAge: 60 * 5,         // 5 минут\n      },\n\n      // Картинки: сначала кэш, подгрузка из сети при промахе\n      {\n        match: '*.{png,jpg,jpeg,gif,svg,webp,ico}',\n        strategy: 'CacheFirst',\n        cache: 'images',\n        maxAge: 60 * 60 * 24 * 30,  // 30 дней\n        maxEntries: 100,             // максимум 100 картинок в кэше\n      },\n\n      // Шрифты: долгий кэш, редко меняются\n      {\n        match: '*.{woff,woff2,ttf,eot}',\n        strategy: 'CacheFirst',\n        cache: 'fonts',\n        maxAge: 60 * 60 * 24 * 365, // 1 год\n      },\n\n      // Страницы авторизации: никогда не кэшировать\n      {\n        match: '/auth/**',\n        strategy: 'NetworkOnly',\n      },\n\n      // Всё остальное: отдать кэш сразу, обновить в фоне\n      {\n        match: '/**',\n        strategy: 'StaleWhileRevalidate',\n      },\n    ],\n  },\n\n  // ─── Push-уведомления ──────────────────────────────────\n  // Настройки push-уведомлений в сгенерированном SW.\n  // serverUrl, appId, apiKey записываются в public/pwa-push.json при generate —\n  // createPushClient() подтягивает их автоматически.\n  notifications: {\n    // Включить обработчики push/notificationclick в SW.\n    // Если false — push-код не генерируется.\n    // Дефолт: true\n    enabled: true,\n\n    // Иконка уведомления по умолчанию (если не передана в push payload).\n    // Дефолт: '/icons/icon-192.png'\n    defaultIcon: '/icons/icon-192.png',\n\n    // Badge — маленькая иконка в status bar (Android).\n    // Дефолт: '/icons/badge-72.png'\n    badge: '/icons/badge-72.png',\n\n    // VAPID public key для подписки на push-уведомления.\n    // Если указан — createPushClient() использует его напрямую,\n    // без запроса к серверу GET /apps/:appId/vapid-key.\n    // Дефолт: '' (запрашивается с сервера)\n    vapidPublicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkGs-GDq...',\n\n    // URL push-сервера. Подтягивается в createPushClient() автоматически.\n    serverUrl: 'https://push.example.com',\n\n    // Идентификатор приложения на push-сервере.\n    appId: 'my-app',\n\n    // API-ключ для аутентификации на push-сервере.\n    apiKey: 'your-api-key',\n  },\n\n  // ─── Выходная директория ───────────────────────────────\n  // Куда складывать сгенерированные иконки.\n  // Дефолт: './public/icons'\n  outDir: './public/icons',\n})\n```\n\n---\n\n### Конфиг: `icon`\n\n```ts\n{ icon: './src/assets/logo.png' }\n```\n\nПуть к исходному изображению. Требования:\n- Формат: PNG (рекомендуется), JPEG, WebP, TIFF, GIF, SVG\n- Минимальный размер: 512x512 пикселей\n- Рекомендуется: квадратное изображение 1024x1024 для лучшего качества\n\nЕсли не указан, запускается автопоиск по 10 стандартным путям:\n\n| Приоритет | Путь |\n|-----------|------|\n| 1 | `icon.png` |\n| 2 | `logo.png` |\n| 3 | `favicon.png` |\n| 4 | `app-icon.png` |\n| 5 | `src/assets/icon.png` |\n| 6 | `src/assets/logo.png` |\n| 7 | `assets/icon.png` |\n| 8 | `assets/logo.png` |\n| 9 | `public/icon.png` |\n| 10 | `public/logo.png` |\n\nПервый найденный файл используется. Если ничего не найдено — генерация иконок пропускается с предупреждением, остальное (manifest, SW) генерируется.\n\n---\n\n### Конфиг: `manifest`\n\nУправляет содержимым `manifest.json`. Каждое поле опционально.\n\n#### `manifest.name`\n\n```ts\n{ manifest: { name: 'My App' } }\n```\n\nПолное имя приложения. Показывается:\n- В диалоге установки PWA\n- В списке приложений ОС\n- В App Switcher\n\n**Дефолт:** значение `name` из `package.json`, или `'My PWA'`.\n\n#### `manifest.short_name`\n\n```ts\n{ manifest: { short_name: 'App' } }\n```\n\nКороткое имя. Показывается:\n- Под иконкой на рабочем столе\n- В Dock / Taskbar\n\nРекомендуется не более 12 символов — длинные имена обрезаются.\n\n**Дефолт:** значение `name` из `package.json`, или `'PWA'`.\n\n#### `manifest.description`\n\n```ts\n{ manifest: { description: 'Fast and reliable task manager' } }\n```\n\nОписание приложения. Используется при публикации в магазины (Google Play через TWA, Microsoft Store).\n\n**Дефолт:** значение `description` из `package.json`, или `''`.\n\n#### `manifest.theme_color`\n\n```ts\n{ manifest: { theme_color: '#4285f4' } }\n```\n\nЦвет темы. Влияет на:\n- Цвет адресной строки в Chrome Android\n- Цвет Title Bar в десктопном PWA\n- Цвет системного UI при переключении приложений\n\n**Дефолт:** `'#ffffff'`\n\n#### `manifest.background_color`\n\n```ts\n{ manifest: { background_color: '#f5f5f5' } }\n```\n\nЦвет фона splash screen, который показывается при запуске PWA до загрузки первой страницы. Рекомендуется совпадать с фоном приложения для плавного перехода.\n\n**Дефолт:** `'#ffffff'`\n\n#### `manifest.display`\n\n```ts\n{ manifest: { display: 'standalone' } }\n```\n\nРежим отображения. Определяет, сколько браузерного UI видно пользователю:\n\n| Значение | Описание |\n|----------|----------|\n| `'standalone'` | Как нативное приложение. Без адресной строки, без табов. Есть status bar. **Рекомендуется.** |\n| `'fullscreen'` | Весь экран. Без status bar. Подходит для игр и медиа. |\n| `'minimal-ui'` | Минимальная навигация браузера (назад/вперёд). |\n| `'browser'` | Обычная вкладка. Не имеет смысла для PWA, но валидное значение. |\n\n**Дефолт:** `'standalone'`\n\n#### `manifest.start_url`\n\n```ts\n{ manifest: { start_url: '/app' } }\n```\n\nURL, который открывается при запуске PWA с рабочего стола. Можно добавить query-параметры для трекинга: `'/app?source=pwa'`.\n\n**Дефолт:** `'/'`\n\n#### `manifest.scope`\n\n```ts\n{ manifest: { scope: '/app/' } }\n```\n\nОграничивает навигацию внутри PWA. Если пользователь переходит по ссылке за пределами scope — открывается обычный браузер. Например, `scope: '/app/'` означает, что `/app/settings` работает внутри PWA, а `/blog` откроется в браузере.\n\n**Дефолт:** `'/'`\n\n#### `manifest.lang`\n\n```ts\n{ manifest: { lang: 'ru' } }\n```\n\nОсновной язык приложения. BCP 47 тег (`'en'`, `'ru'`, `'de'`, `'ja'`, `'zh-CN'` и т.д.).\n\n**Дефолт:** `'en'`\n\n#### `manifest.orientation`\n\n```ts\n{ manifest: { orientation: 'portrait' } }\n```\n\nПредпочтительная ориентация экрана:\n\n| Значение | Описание |\n|----------|----------|\n| `'any'` | Любая ориентация. Приложение подстраивается. |\n| `'natural'` | Естественная ориентация устройства (портрет для телефона, ландшафт для планшета). |\n| `'landscape'` | Горизонтальная. |\n| `'portrait'` | Вертикальная. |\n\n**Дефолт:** `'any'`\n\n---\n\n### Конфиг: `sw`\n\nУправляет генерацией Service Worker.\n\n#### `sw.output`\n\n```ts\n{ sw: { output: './dist/sw.js' } }\n```\n\nПуть, куда записать сгенерированный `sw.js`. Директории создаются автоматически.\n\n**Дефолт:** `'./public/sw.js'`\n\n#### `sw.precache`\n\n```ts\n{ sw: { precache: ['./public/**/*.{html,css,js}'] } }\n```\n\nGlob-паттерны файлов для предварительного кэширования. Все совпавшие файлы будут загружены и закэшированы при установке Service Worker — ещё до того, как пользователь откроет эти страницы.\n\nКогда использовать:\n- App shell (HTML, CSS, JS)\n- Критичные ресурсы для offline-работы\n- Страницы, которые должны открываться мгновенно\n\nКогда НЕ использовать:\n- Большие файлы (видео, архивы) — замедлят установку SW\n- Часто меняющийся контент — придётся обновлять SW при каждом изменении\n\nПримеры паттернов:\n```ts\nprecache: [\n  './public/index.html',                   // конкретный файл\n  './public/**/*.css',                      // все CSS\n  './public/assets/**/*.{js,css}',          // JS и CSS из assets\n  './public/**/*.{html,css,js,json}',       // все основные ресурсы\n]\n```\n\nФайлы преобразуются в URL-пути (`./public/assets/main.css` → `/assets/main.css`).\n\n**Дефолт:** `[]` (ничего не предзагружается)\n\n#### `sw.routes`\n\n```ts\n{ sw: { routes: [{ match: '...', strategy: '...', ... }] } }\n```\n\nМассив правил кэширования. Каждый запрос проверяется по порядку — первое совпадение выигрывает.\n\nПараметры каждого маршрута:\n\n| Поле | Тип | Обязательно | Описание |\n|------|-----|-------------|----------|\n| `match` | `string` | да | URL-паттерн |\n| `strategy` | `CacheStrategy` | да | Стратегия кэширования |\n| `cache` | `string` | нет | Имя кэша (автогенерируется если не указано) |\n| `maxAge` | `number` | нет | TTL в секундах |\n| `maxEntries` | `number` | нет | Максимум записей в кэше |\n\n**Синтаксис `match`:**\n\n| Паттерн | Значение | Пример совпадения |\n|---------|----------|-------------------|\n| `/api/**` | Любой путь начинающийся с `/api/` | `/api/users`, `/api/v2/posts/123` |\n| `*.{png,jpg}` | Файлы с расширениями `.png` или `.jpg` | `/images/photo.png`, `/logo.jpg` |\n| `/fonts/**` | Все шрифты в `/fonts/` | `/fonts/Inter.woff2` |\n| `/**` | Абсолютно любой путь (catch-all) | Всё |\n| `*` | Любой сегмент (без `/`) | Один уровень пути |\n| `**` | Любое количество сегментов | Любая вложенность |\n\n**Стратегии (`strategy`):**\n\n| Стратегия | Поведение | Лучше всего для |\n|-----------|-----------|-----------------|\n| `'CacheFirst'` | Кэш → сеть (при промахе или истечении) | Статика: картинки, шрифты, CSS/JS |\n| `'NetworkFirst'` | Сеть (3с таймаут) → кэш | API, динамический контент |\n| `'StaleWhileRevalidate'` | Кэш сразу + обновление в фоне | Страницы, JSON-данные |\n| `'NetworkOnly'` | Только сеть, без кэширования | Авторизация, платежи, аналитика |\n| `'CacheOnly'` | Только кэш, без сети | Предзагруженные ресурсы |\n\n**`maxAge`** — время жизни кэша в секундах. После истечения запись считается устаревшей. Удобные значения:\n\n| Время | Секунды | Выражение |\n|-------|---------|-----------|\n| 5 минут | 300 | `60 * 5` |\n| 1 час | 3600 | `60 * 60` |\n| 1 день | 86400 | `60 * 60 * 24` |\n| 7 дней | 604800 | `60 * 60 * 24 * 7` |\n| 30 дней | 2592000 | `60 * 60 * 24 * 30` |\n| 1 год | 31536000 | `60 * 60 * 24 * 365` |\n\n**`maxEntries`** — максимальное количество записей в кэше. При превышении удаляются самые старые. Защищает от переполнения хранилища.\n\n**`cache`** — имя кэша в Cache Storage браузера. Если не указано, генерируется автоматически из стратегии (например, `rt-networkfirst`). Рекомендуется указывать для удобства отладки в DevTools.\n\n#### Дефолтные маршруты\n\nЕсли `sw.routes` не указаны, используются:\n\n| Паттерн | Стратегия | Кэш | TTL | Лимит |\n|---------|-----------|-----|-----|-------|\n| `/api/**` | NetworkFirst | `api-cache` | 5 мин | — |\n| `*.{png,jpg,jpeg,gif,svg,webp,ico}` | CacheFirst | `images` | 30 дней | 100 |\n| `*.{woff,woff2,ttf,eot}` | CacheFirst | `fonts` | 1 год | — |\n| `/**` | StaleWhileRevalidate | — | — | — |\n\nЭтот набор покрывает типичное SPA/MPA: API через сеть с fallback, статика из кэша, остальное — stale-while-revalidate.\n\n#### Порядок маршрутов\n\nПорядок массива `routes` имеет значение. Первый совпавший паттерн обрабатывает запрос. Общие паттерны (`/**`) должны быть в конце:\n\n```ts\nroutes: [\n  { match: '/api/**', strategy: 'NetworkFirst', ... },     // 1. конкретное\n  { match: '*.{png,jpg}', strategy: 'CacheFirst', ... },   // 2. по расширению\n  { match: '/**', strategy: 'StaleWhileRevalidate' },      // 3. catch-all в конце\n]\n```\n\nЕсли поставить `/**` первым — все запросы будут обработаны StaleWhileRevalidate, и до остальных правил дело не дойдёт.\n\n---\n\n### Конфиг: `notifications`\n\nНастройки push-уведомлений в сгенерированном Service Worker. Можно полностью отключить push-обработчики или настроить иконки-fallback. Также содержит параметры подключения к push-серверу (`serverUrl`, `appId`, `apiKey`) — при `pwa-lib generate` они записываются в `public/pwa-push.json`, и `createPushClient()` подтягивает их автоматически.\n\n#### `notifications.enabled`\n\n```ts\n{ notifications: { enabled: false } }\n```\n\nВключает или отключает обработчики `push` и `notificationclick` в сгенерированном SW. Если `false` — push-код не попадает в SW.\n\n**Дефолт:** `true`\n\n#### `notifications.defaultIcon`\n\n```ts\n{ notifications: { defaultIcon: '/icons/icon-192.png' } }\n```\n\nИконка уведомления. Показывается рядом с текстом в панели уведомлений и в развёрнутом виде.\n\n**Дефолт:** `'/icons/icon-192.png'`\n\n#### `notifications.badge`\n\n```ts\n{ notifications: { badge: '/icons/badge-72.png' } }\n```\n\nBadge — маленькая монохромная иконка в status bar Android. Показывается, когда уведомление свёрнуто. Рекомендуется квадратная, 72x72 или 96x96, с прозрачным фоном.\n\n**Дефолт:** `'/icons/badge-72.png'`\n\n#### `notifications.vapidPublicKey`\n\n```ts\n{ notifications: { vapidPublicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkGs-GDq...' } }\n```\n\nVAPID public key для подписки на push-уведомления. Если указан, `createPushClient()` использует его напрямую — без запроса `GET /apps/:appId/vapid-key` к серверу. Это убирает лишний сетевой запрос при подписке.\n\n**Дефолт:** `''` (ключ запрашивается с сервера)\n\n#### `notifications.serverUrl`\n\n```ts\n{ notifications: { serverUrl: 'https://push.example.com' } }\n```\n\nURL push-сервера. При `pwa-lib generate` записывается в `public/pwa-push.json` и автоматически подтягивается в `createPushClient()`.\n\n**Дефолт:** `''`\n\n#### `notifications.appId`\n\n```ts\n{ notifications: { appId: 'my-app' } }\n```\n\nИдентификатор приложения на push-сервере. При `pwa-lib generate` записывается в `public/pwa-push.json` и автоматически подтягивается в `createPushClient()`.\n\n**Дефолт:** `''`\n\n#### `notifications.apiKey`\n\n```ts\n{ notifications: { apiKey: 'your-api-key' } }\n```\n\nAPI-ключ для аутентификации на push-сервере. При `pwa-lib generate` записывается в `public/pwa-push.json` и автоматически подтягивается в `createPushClient()`.\n\n**Дефолт:** `''`\n\n---\n\n### Конфиг: `outDir`\n\n```ts\n{ outDir: './public/icons' }\n```\n\nДиректория для сгенерированных иконок. Создаётся автоматически. URL-пути в `manifest.json` строятся относительно `public/` (то есть `./public/icons` → `/icons/icon-192.png` в манифесте).\n\n**Дефолт:** `'./public/icons'`\n\n---\n\n### Примеры конфигов\n\n#### Минимальный\n\nПросто указать иконку, остальное на дефолтах:\n\n```ts\nexport default {\n  icon: './logo.png',\n}\n```\n\n#### Только manifest\n\nНастроить внешний вид PWA, всё остальное по умолчанию:\n\n```ts\nimport { defineConfig } from 'pwa-lib/config'\n\nexport default defineConfig({\n  manifest: {\n    name: 'Task Manager',\n    short_name: 'Tasks',\n    theme_color: '#1a73e8',\n    background_color: '#fafafa',\n    lang: 'ru',\n  },\n})\n```\n\n#### Кастомные стратегии кэширования\n\nДля приложения с GraphQL API и CDN:\n\n```ts\nimport { defineConfig } from 'pwa-lib/config'\n\nexport default defineConfig({\n  sw: {\n    routes: [\n      // GraphQL — всегда сеть\n      {\n        match: '/graphql',\n        strategy: 'NetworkOnly',\n      },\n      // CDN-картинки — долгий кэш\n      {\n        match: '*.{png,jpg,jpeg,webp,avif}',\n        strategy: 'CacheFirst',\n        cache: 'cdn-images',\n        maxAge: 60 * 60 * 24 * 90,  // 90 дней\n        maxEntries: 200,\n      },\n      // HTML-страницы — быстро из кэша, обновление в фоне\n      {\n        match: '*.html',\n        strategy: 'StaleWhileRevalidate',\n        cache: 'pages',\n      },\n      // Остальное\n      {\n        match: '/**',\n        strategy: 'NetworkFirst',\n        cache: 'default',\n        maxAge: 60 * 60,\n      },\n    ],\n  },\n})\n```\n\n#### Offline-first приложение\n\nPrecache app shell для полной offline-работы:\n\n```ts\nimport { defineConfig } from 'pwa-lib/config'\n\nexport default defineConfig({\n  icon: './src/assets/app-icon.png',\n  manifest: {\n    name: 'Offline Notes',\n    short_name: 'Notes',\n    display: 'standalone',\n    theme_color: '#212121',\n    background_color: '#212121',\n  },\n  sw: {\n    precache: [\n      './public/index.html',\n      './public/assets/**/*.{js,css}',\n      './public/fonts/**/*.woff2',\n    ],\n    routes: [\n      { match: '/api/**', strategy: 'NetworkFirst', cache: 'api', maxAge: 60 * 10 },\n      { match: '/**', strategy: 'CacheFirst', cache: 'app-shell' },\n    ],\n  },\n})\n```\n\n#### Полный конфиг\n\nВсе опции явно:\n\n```ts\nimport { defineConfig } from 'pwa-lib/config'\n\nexport default defineConfig({\n  icon: './src/assets/icon-1024.png',\n  outDir: './public/icons',\n  manifest: {\n    name: 'My Production App',\n    short_name: 'MyApp',\n    description: 'A production-ready progressive web application',\n    theme_color: '#4285f4',\n    background_color: '#ffffff',\n    display: 'standalone',\n    start_url: '/?source=pwa',\n    scope: '/',\n    lang: 'en',\n    orientation: 'portrait',\n  },\n  sw: {\n    output: './public/sw.js',\n    precache: ['./public/**/*.{html,css,js}'],\n    routes: [\n      { match: '/auth/**', strategy: 'NetworkOnly' },\n      { match: '/api/**', strategy: 'NetworkFirst', cache: 'api', maxAge: 300 },\n      { match: '*.{png,jpg,jpeg,gif,svg,webp}', strategy: 'CacheFirst', cache: 'images', maxAge: 2592000, maxEntries: 100 },\n      { match: '*.{woff,woff2}', strategy: 'CacheFirst', cache: 'fonts', maxAge: 31536000 },\n      { match: '*.{js,css}', strategy: 'StaleWhileRevalidate', cache: 'static', maxEntries: 50 },\n      { match: '/**', strategy: 'NetworkFirst', cache: 'pages', maxAge: 86400 },\n    ],\n  },\n  notifications: {\n    defaultIcon: '/icons/icon-192.png',\n    badge: '/icons/icon-72.png',\n    vapidPublicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkGs-GDq...',\n    serverUrl: 'https://push.example.com',\n    appId: 'my-app',\n    apiKey: 'your-api-key',\n  },\n})\n```\n\n---\n\n## CLI-команды\n\n### `pwa-lib init`\n\nСоздаёт `pwa.config.ts` с закомментированными примерами.\n\n```bash\npwa-lib init          # создать конфиг\npwa-lib init --yes    # без вопросов, сразу дефолты\n```\n\n| Флаг | Алиас | Описание |\n|------|-------|----------|\n| `--yes` | `-y` | Пропустить вопросы |\n\nЕсли `pwa.config.ts` уже существует — пропускает с предупреждением.\n\n### `pwa-lib generate`\n\nОсновная команда. Генерирует всё разом: иконки + manifest + Service Worker + push-конфиг.\n\n```bash\npwa-lib generate\n```\n\nЧитает `pwa.config.ts` (или дефолты если конфига нет). Если иконка не найдена — генерация иконок пропускается, остальное генерируется. Если заданы `notifications.serverUrl` / `appId` / `apiKey` — дополнительно генерирует `public/pwa-push.json` для автоматической конфигурации `createPushClient()`.\n\n### `pwa-lib icons`\n\nГенерация только иконок.\n\n```bash\npwa-lib icons\npwa-lib icons --input ./logo.png\npwa-lib icons --input ./logo.png --output ./public/icons\n```\n\n| Флаг | Алиас | Описание |\n|------|-------|----------|\n| `--input` | `-i` | Путь к исходной иконке (>= 512x512) |\n| `--output` | `-o` | Директория для иконок |\n\nЕсли флаги не указаны — берёт значения из конфига / автодетекта.\n\n### `pwa-lib dev`\n\nWatch mode. Следит за конфигом и `public/`, перегенерирует SW и `pwa-push.json` при изменениях.\n\n```bash\npwa-lib dev\n```\n\nСледит за:\n- `pwa.config.ts` / `pwa.config.js` / `pwa.config.mjs`\n- `public/` (рекурсивно)\n\nДебаунс: 300мс для конфига, 500мс для `public/`.\n\n---\n\n## Генерация иконок\n\nИз одного изображения (>= 512x512) генерируются:\n\n| Размер | Файл | Назначение |\n|--------|------|------------|\n| 16x16 | `icon-16.png` | favicon |\n| 32x32 | `icon-32.png` | favicon |\n| 48x48 | `icon-48.png` | favicon |\n| 72x72 | `icon-72.png` | Android badge |\n| 96x96 | `icon-96.png` | Android |\n| 128x128 | `icon-128.png` | Android |\n| 144x144 | `icon-144.png` | Android |\n| 152x152 | `icon-152.png` | iOS |\n| 180x180 | `apple-touch-icon.png` | Apple touch icon |\n| 192x192 | `icon-192.png` | Android / manifest |\n| 384x384 | `icon-384.png` | Android |\n| 512x512 | `icon-512.png` | Android / splash |\n| 512x512 | `icon-512-maskable.png` | Maskable icon |\n| multi | `favicon.ico` | ICO (16 + 32 + 48) |\n\n- Все PNG с прозрачным фоном (`contain` resize).\n- Maskable-иконка имеет 10% белый padding для safe zone (контент занимает 80% площади).\n- `favicon.ico` — контейнер с тремя PNG внутри (16, 32, 48).\n\n---\n\n## Стратегии кэширования\n\n### `CacheFirst`\n\nСначала кэш, потом сеть.\n\n```\nЗапрос → Кэш есть и не истёк? → Да → Ответ из кэша\n                                 → Нет → Запрос в сеть → Сохранить в кэш → Ответ\n```\n\n- Поддерживает `maxAge` — при истечении идёт в сеть\n- Поддерживает `maxEntries` — удаляет старые записи\n- При ошибке сети и наличии просроченного кэша — вернёт просроченный\n- Лучше всего для: картинки, шрифты, CSS/JS с хэшами\n\n### `NetworkFirst`\n\nСначала сеть, потом кэш.\n\n```\nЗапрос → Сеть (таймаут 3с) → Успех → Сохранить в кэш → Ответ\n                             → Таймаут/ошибка → Кэш есть? → Да → Ответ из кэша\n                                                            → Нет → Ошибка\n```\n\n- Таймаут 3 секунды — если сеть не ответила, сразу кэш\n- Поддерживает `maxEntries`\n- Лучше всего для: API, JSON-данные, динамический контент\n\n### `StaleWhileRevalidate`\n\nМгновенный ответ из кэша + обновление в фоне.\n\n```\nЗапрос → Кэш есть? → Да → Ответ из кэша (мгновенно)\n                          → Фоновый запрос в сеть → Обновить кэш\n                     → Нет → Запрос в сеть → Ответ\n```\n\n- Самая быстрая стратегия при наличии кэша\n- Пользователь видит предыдущую версию, а при следующем визите — обновлённую\n- Поддерживает `maxEntries`\n- Лучше всего для: HTML-страницы, не-критичные данные\n\n### `NetworkOnly`\n\nТолько сеть, без кэширования.\n\n```\nЗапрос → Сеть → Ответ (или ошибка)\n```\n\n- Никогда не кэширует и не читает из кэша\n- Лучше всего для: авторизация, платежи, аналитика, real-time данные\n\n### `CacheOnly`\n\nТолько кэш, без сети.\n\n```\nЗапрос → Кэш есть? → Да → Ответ\n                     → Нет → 404\n```\n\n- Работает только с precache или ранее закэшированными ресурсами\n- Лучше всего для: offline app shell, критичные ресурсы\n\n---\n\n## Сгенерированный Service Worker\n\nSW генерируется как готовый JS-файл. Без Workbox, без runtime-зависимостей. Содержит:\n\n- **Install** — `skipWaiting()` для мгновенной активации + precache (если настроен)\n- **Activate** — удаление неизвестных кэшей, `clients.claim()` для захвата контроля\n- **Fetch** — маршрутизация запросов по URL-паттернам к соответствующим стратегиям\n- **Push** — обработка push-уведомлений с настраиваемыми icon/badge\n- **Notification click** — фокус на существующее окно или открытие нового\n\nМаршрутизация: glob-паттерны конвертируются в regex:\n- `**` — любое количество сегментов пути\n- `*` — один сегмент (без `/`)\n- `{ext1,ext2}` — альтернативы\n\nОбрабатываются только `GET`-запросы по `http(s)`. Non-GET, `chrome-extension://` и подобные пропускаются.\n\n---\n\n## Клиентский API\n\nБраузерная библиотека для регистрации SW и push-уведомлений.\n\n```ts\nimport { registerSW, notifications } from 'pwa-lib/client'\n```\n\nPure ESM, zero dependencies, tree-shakeable.\n\n### `registerSW(swUrl?, options?)`\n\nРегистрация Service Worker с колбэками жизненного цикла.\n\n```ts\nconst registration = await registerSW('/sw.js', {\n  onUpdate: (reg) => {\n    // Новая версия SW доступна.\n    // Можно показать пользователю баннер \"Обновление доступно\".\n    console.log('Update available')\n  },\n  onReady: (reg) => {\n    // SW активен и контролирует страницу.\n    console.log('SW ready')\n  },\n  onError: (err) => {\n    // Ошибка регистрации.\n    console.error('SW registration failed', err)\n  },\n})\n```\n\n| Параметр | Тип | Дефолт | Описание |\n|----------|-----|--------|----------|\n| `swUrl` | `string` | `'/sw.js'` | URL service worker файла |\n| `options.onUpdate` | `(reg) => void` | — | Новая версия SW доступна |\n| `options.onReady` | `(reg) => void` | — | SW активен |\n| `options.onError` | `(err) => void` | — | Ошибка регистрации |\n\nБросает ошибку если Service Workers не поддерживаются.\n\n### `notifications`\n\nAPI для push-уведомлений.\n\n```ts\n// 1. Проверить поддержку\nif (notifications.isSupported()) {\n\n  // 2. Запросить разрешение\n  const permission = await notifications.requestPermission()\n\n  if (permission === 'granted') {\n\n    // 3. Подписаться на push (нужен VAPID ключ сервера)\n    const subscription = await notifications.subscribe({\n      applicationServerKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkGs-GDq...',\n    })\n\n    // 4. Отправить подписку на сервер\n    await fetch('/api/push/subscribe', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(subscription),\n    })\n  }\n}\n\n// Получить текущую подписку\nconst current = await notifications.getSubscription()\n\n// Отписаться\nconst success = await notifications.unsubscribe()\n```\n\n| Метод | Возвращает | Описание |\n|-------|------------|----------|\n| `isSupported()` | `boolean` | Проверка поддержки Notification + PushManager + SW |\n| `requestPermission()` | `Promise<NotificationPermission>` | Запрос разрешения у пользователя (`'granted'` / `'denied'` / `'default'`) |\n| `subscribe(options)` | `Promise<PushSubscription>` | Подписка с VAPID ключом |\n| `getSubscription()` | `Promise<PushSubscription \\| null>` | Текущая подписка (или `null`) |\n| `unsubscribe()` | `Promise<boolean>` | Отписка (`true` если была подписка) |\n\nФормат push-payload для сервера:\n```json\n{\n  \"title\": \"Новое сообщение\",\n  \"body\": \"У вас 3 непрочитанных сообщения\",\n  \"icon\": \"/icons/icon-192.png\",\n  \"badge\": \"/icons/badge-72.png\",\n  \"data\": { \"url\": \"/messages\" },\n  \"actions\": [\n    { \"action\": \"open\", \"title\": \"Открыть\" },\n    { \"action\": \"dismiss\", \"title\": \"Закрыть\" }\n  ],\n  \"tag\": \"messages\",\n  \"renotify\": true\n}\n```\n\nПри клике на уведомление SW откроет `data.url` (или `/` если не указан). Если окно приложения уже открыто — сфокусирует его.\n\n### `createPushClient(config?)`\n\nВысокоуровневый клиент для полного flow push-уведомлений с сервером. Один вызов `subscribe()` делает всё: проверяет поддержку, запрашивает разрешение, получает VAPID-ключ, подписывает через PushManager, отправляет подписку на сервер. Разработчику остаётся только привязать к кнопке.\n\nКонфиг подтягивается автоматически из `pwa-push.json` (генерируется при `pwa-lib generate`). Достаточно указать `serverUrl`, `appId`, `apiKey` в `pwa.config.ts` — и в клиентском коде ничего передавать не нужно:\n\n```ts\nimport { createPushClient } from 'pwa-lib/client'\n\nconst push = createPushClient()\nawait push.subscribe()\n```\n\nЕсли нужно переопределить конфиг вручную — можно передать явно:\n\n```ts\nconst push = createPushClient({\n  serverUrl: 'https://push.example.com',\n  appId: 'my-app',\n  apiKey: 'your-api-key',\n})\n```\n\n#### Полный пример: кнопка подписки\n\nТипичный сценарий — кнопка «Подписаться на уведомления» в UI:\n\n```ts\nimport { registerSW, notifications, createPushClient, PushClientError } from 'pwa-lib/client'\n\n// 1. Регистрируем SW (обязательно до работы с push)\nawait registerSW('/sw.js')\n\n// 2. Создаём push-клиент (конфиг подтягивается из pwa-push.json автоматически)\nconst push = createPushClient()\n\n// 3. Определяем начальное состояние кнопки\nconst btn = document.querySelector<HTMLButtonElement>('#push-btn')!\nconst status = document.querySelector<HTMLSpanElement>('#push-status')!\n\nasync function updateUI() {\n  const subscription = await notifications.getSubscription()\n  if (subscription) {\n    btn.textContent = 'Отписаться от уведомлений'\n    status.textContent = 'Уведомления включены'\n  } else {\n    btn.textContent = 'Подписаться на уведомления'\n    status.textContent = ''\n  }\n}\n\n// 4. Скрываем кнопку если браузер не поддерживает push\nif (!notifications.isSupported()) {\n  btn.hidden = true\n  status.textContent = 'Ваш браузер не поддерживает push-уведомления'\n} else {\n  await updateUI()\n}\n\n// 5. Обработчик клика — toggle подписки\nbtn.addEventListener('click', async () => {\n  btn.disabled = true\n\n  try {\n    const subscription = await notifications.getSubscription()\n\n    if (subscription) {\n      await push.unsubscribe()\n    } else {\n      await push.subscribe()\n    }\n\n    await updateUI()\n  } catch (err) {\n    if (err instanceof PushClientError) {\n      switch (err.code) {\n        case 'PERMISSION_DENIED':\n          status.textContent = 'Вы запретили уведомления. Разрешите в настройках браузера.'\n          break\n        case 'NETWORK_ERROR':\n        case 'VAPID_FETCH_FAILED':\n        case 'SERVER_SUBSCRIBE_FAILED':\n        case 'SERVER_UNSUBSCRIBE_FAILED':\n          status.textContent = 'Ошибка сервера. Попробуйте позже.'\n          break\n        default:\n          status.textContent = 'Не удалось подписаться на уведомления.'\n      }\n    }\n  } finally {\n    btn.disabled = false\n  }\n})\n```\n\n#### Пример с React\n\n```tsx\nimport { useEffect, useState } from 'react'\nimport { notifications, createPushClient, PushClientError } from 'pwa-lib/client'\n\nconst push = createPushClient()\n\nfunction PushButton() {\n  const [subscribed, setSubscribed] = useState(false)\n  const [loading, setLoading] = useState(false)\n  const [error, setError] = useState('')\n  const supported = notifications.isSupported()\n\n  useEffect(() => {\n    if (!supported) return\n    notifications.getSubscription().then((sub) => setSubscribed(!!sub))\n  }, [])\n\n  if (!supported) return null\n\n  async function handleClick() {\n    setLoading(true)\n    setError('')\n\n    try {\n      if (subscribed) {\n        await push.unsubscribe()\n        setSubscribed(false)\n      } else {\n        await push.subscribe()\n        setSubscribed(true)\n      }\n    } catch (err) {\n      if (err instanceof PushClientError) {\n        if (err.code === 'PERMISSION_DENIED') {\n          setError('Разрешите уведомления в настройках браузера')\n        } else {\n          setError('Не удалось. Попробуйте позже.')\n        }\n      }\n    } finally {\n      setLoading(false)\n    }\n  }\n\n  return (\n    <>\n      <button onClick={handleClick} disabled={loading}>\n        {loading ? 'Загрузка...' : subscribed ? 'Отписаться' : 'Подписаться на уведомления'}\n      </button>\n      {error && <p>{error}</p>}\n    </>\n  )\n}\n```\n\n#### Счётчик подписчиков\n\n```ts\nconst count = await push.getSubscriberCount()\ndocument.querySelector('#count')!.textContent = `${count} подписчиков`\n```\n\n#### API методов\n\n| Метод | Возвращает | Описание |\n|-------|------------|----------|\n| `subscribe()` | `Promise<PushSubscriptionData>` | Полный flow: поддержка → разрешение → VAPID → PushManager → сервер |\n| `unsubscribe()` | `Promise<void>` | Удаление на сервере → отписка в браузере. Если подписки нет — ничего не делает |\n| `getSubscriberCount()` | `Promise<number>` | Количество подписчиков с сервера |\n\n`subscribe()` возвращает `PushSubscriptionData`:\n```ts\n{\n  endpoint: 'https://fcm.googleapis.com/fcm/send/...',\n  keys: { p256dh: '...', auth: '...' }\n}\n```\n\n`unsubscribe()` — server-first: сначала удаляем на сервере, потом в браузере. Если сервер упал — подписка в браузере остаётся, можно повторить.\n\n#### Обработка ошибок\n\nВсе методы бросают `PushClientError` с типизированным `code`:\n\n| Код | Когда |\n|-----|-------|\n| `NOT_SUPPORTED` | Браузер не поддерживает Notification / PushManager / SW |\n| `PERMISSION_DENIED` | Пользователь отклонил разрешение на уведомления |\n| `VAPID_FETCH_FAILED` | Сервер не вернул VAPID-ключ |\n| `SUBSCRIBE_FAILED` | Ошибка PushManager.subscribe() |\n| `SERVER_SUBSCRIBE_FAILED` | Сервер не принял подписку |\n| `SERVER_UNSUBSCRIBE_FAILED` | Сервер не удалил подписку |\n| `UNSUBSCRIBE_FAILED` | Ошибка отписки в браузере |\n| `NETWORK_ERROR` | Сетевой запрос не прошёл |\n\nRetry-логика — ответственность потребителя. `code` даёт достаточно информации, чтобы решить что показать пользователю и стоит ли повторять.\n\n#### Серверный API\n\n`createPushClient` ожидает следующие эндпоинты:\n\n| Метод | Эндпоинт | Тело / Ответ |\n|-------|----------|--------------|\n| `GET` | `/api/apps/:appId/vapid-public-key` | → `{ vapidPublicKey: string }` — необязателен если `vapidPublicKey` передан в конфиге |\n| `POST` | `/api/apps/:appId/subscribe` | `PushSubscriptionData` → `201` |\n| `POST` | `/api/apps/:appId/unsubscribe` | `{ endpoint: string }` → `200` |\n| `GET` | `/api/apps/:appId/subscribers/count` | → `{ count: number }` |\n\nВсе запросы (кроме `vapid-public-key`) содержат заголовки `Content-Type: application/json` и `X-API-Key: <apiKey>`.\n\n---\n\n## TypeScript\n\nВсе экспорты полностью типизированы.\n\n```ts\nimport type {\n  PwaConfig,\n  ResolvedPwaConfig,\n  CacheStrategy,\n  RouteConfig,\n  SwConfig,\n  ManifestConfig,\n  NotificationsConfig,\n  IconEntry,\n} from 'pwa-lib'\n```\n\n| Тип | Описание |\n|-----|----------|\n| `PwaConfig` | Пользовательский конфиг (все поля опциональны) |\n| `ResolvedPwaConfig` | Конфиг после merge с дефолтами (все поля обязательны) |\n| `CacheStrategy` | `'CacheFirst' \\| 'NetworkFirst' \\| 'StaleWhileRevalidate' \\| 'NetworkOnly' \\| 'CacheOnly'` |\n| `RouteConfig` | Маршрут: `match`, `strategy`, `cache?`, `maxAge?`, `maxEntries?` |\n| `SwConfig` | `output?`, `precache?`, `routes?` |\n| `ManifestConfig` | Все поля manifest (все опциональны) |\n| `NotificationsConfig` | `enabled?`, `defaultIcon?`, `badge?`, `vapidPublicKey?`, `serverUrl?`, `appId?`, `apiKey?` |\n| `IconEntry` | `size`, `name`, `purpose?` |\n| `PushClientConfig` | `serverUrl?`, `appId?`, `apiKey?`, `vapidPublicKey?` |\n| `PushClient` | Интерфейс объекта от `createPushClient()`: `subscribe()`, `unsubscribe()`, `getSubscriberCount()` |\n| `PushSubscriptionData` | `endpoint`, `keys: { p256dh, auth }` |\n| `PushClientErrorCode` | Union кодов ошибок: `'NOT_SUPPORTED' \\| 'PERMISSION_DENIED' \\| ...` |\n\n## Экспорты пакета\n\n| Путь импорта | Описание |\n|--------------|----------|\n| `pwa-lib` | Типы, `defineConfig()`, константы (`DEFAULT_CONFIG`, `DEFAULT_ROUTES`, `ICON_SEARCH_PATHS`) |\n| `pwa-lib/config` | Алиас для `pwa-lib` (для использования в конфиг-файлах) |\n| `pwa-lib/client` | Браузерный API: `registerSW()`, `notifications`, `createPushClient()`, `PushClientError` |\n\n---\n\n## Структура проекта\n\n```\npwa-lib/\n├── src/\n│   ├── cli/                    # CLI точка входа\n│   │   ├── index.ts            # bin entry — citty router\n│   │   ├── commands/\n│   │   │   ├── init.ts         # pwa-lib init\n│   │   │   ├── generate.ts     # pwa-lib generate\n│   │   │   ├── icons.ts        # pwa-lib icons\n│   │   │   └── dev.ts          # pwa-lib dev\n│   │   └── utils/\n│   │       ├── logger.ts       # consola логгер\n│   │       └── config-loader.ts # Загрузка конфига через jiti + defu\n│   ├── core/                   # Движки генерации\n│   │   ├── icons/\n│   │   │   ├── generator.ts    # sharp pipeline + ICO builder\n│   │   │   └── sizes.ts        # Пресеты размеров иконок\n│   │   ├── sw/\n│   │   │   ├── builder.ts      # Сборка финального sw.js\n│   │   │   ├── strategies.ts   # 5 стратегий кэширования\n│   │   │   ├── runtime.ts      # Runtime-конфиг для SW\n│   │   │   └── templates/\n│   │   │       ├── sw-core.ts  # Install/activate lifecycle\n│   │   │       ├── caching.ts  # Fetch handler + route matching\n│   │   │       ├── precache.ts # Precache-список\n│   │   │       └── push.ts     # Push-уведомления\n│   │   └── manifest/\n│   │       └── generator.ts    # Генерация manifest.json\n│   ├── client/                 # Браузерная библиотека\n│   │   ├── index.ts            # Экспорты\n│   │   ├── sw-register.ts      # registerSW()\n│   │   ├── notifications.ts    # Push API (низкоуровневый)\n│   │   ├── push-client.ts      # createPushClient() — высокоуровневый push-клиент\n│   │   ├── push-client.types.ts # Типы для push-клиента\n│   │   └── types.ts            # Клиентские типы\n│   └── shared/                 # Общие типы и дефолты\n│       ├── index.ts            # Экспорты\n│       ├── types.ts            # PwaConfig, defineConfig()\n│       └── defaults.ts         # Дефолтные значения\n├── package.json\n├── tsconfig.json\n└── tsup.config.ts              # 3 сборки: CLI, client, shared\n```\n\n## Зависимости\n\n| Пакет | Назначение |\n|-------|------------|\n| [sharp](https://sharp.pixelplumbing.com/) | Нативная обработка изображений для генерации иконок |\n| [citty](https://github.com/unjs/citty) | Лёгкий типизированный CLI-фреймворк |\n| [consola](https://github.com/unjs/consola) | Красивый консольный логгер |\n| [jiti](https://github.com/unjs/jiti) | Загрузка .ts конфигов без ts-node |\n| [defu](https://github.com/unjs/defu) | Глубокий merge конфигов с дефолтами |\n| [globby](https://github.com/sindresorhus/globby) | Glob-паттерны для precache |\n\n## Лицензия\n\nMIT\n","readmeFilename":"README.md"}