From bf72af9ef4b440d44b5ac9afd71c3b18b7673747 Mon Sep 17 00:00:00 2001 From: kamask Date: Sat, 31 Oct 2026 20:24:13 +0300 Subject: [PATCH] feat(dev): proxy /pb to PocketBase in ng serve Add proxy.conf.mjs mirroring `location /pb/` from nginx: the prefix is stripped, target comes from PB_URL (empty value falls back to the default, same as the SSR provider). Cover the config with tests in tests/lint and update docs. Co-Authored-By: Claude Sonnet 5.5 --- .env.example | 1 + README.md | 28 ++++++++++++------------- angular.json | 3 +++ docs/architecture.md | 2 +- docs/testing.md | 7 ++++--- proxy.conf.mjs | 11 ++++++++++ tests/lint/pb-dev-proxy.spec.ts | 36 +++++++++++++++++++++++++++++++++ tsconfig.tests.json | 4 +++- 8 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 proxy.conf.mjs create mode 100644 tests/lint/pb-dev-proxy.spec.ts diff --git a/.env.example b/.env.example index 92b1a32..a1ee8ee 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ # Copy to .env and fill in. .env is not committed. +# PocketBase address for the SSR server and the ng serve /pb/ proxy (ng serve reads it from the environment, not .env). PB_URL=http://127.0.0.1:8090 PB_DIR=pocketbase/pb_data PB_SUPERUSER_EMAIL= diff --git a/README.md b/README.md index 365dae3..25ab180 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,20 @@ pnpm start # приложение на http://localhost:4200/ ## Команды -| Команда | Что делает | -| ----------------------- | ----------------------------------------------------------------------- | -| `pnpm start` | dev-сервер приложения | -| `pnpm build` | production-сборка в `dist/` | -| `pnpm lint` | ESLint | -| `pnpm test` | unit-тесты | -| `pnpm test:api` | API-тесты PocketBase: схема, правила доступа, изоляция пользователей | -| `pnpm test:integration` | репозитории приложения против тестового PocketBase | -| `pnpm test:lint` | тесты конфига ESLint; первый шаг `test`, `test:api`, `test:integration` | -| `pnpm e2e` | сценарии в браузере и проверка доступности (axe) | -| `pnpm e2e:prod` | сценарии на production-сборке | -| `pnpm pb:serve` | PocketBase для разработки | -| `pnpm pb:migrate` | применить миграции | -| `pnpm pb:superuser` | создать суперпользователя или сбросить его пароль | +| Команда | Что делает | +| ----------------------- | ------------------------------------------------------------------------------------- | +| `pnpm start` | dev-сервер приложения | +| `pnpm build` | production-сборка в `dist/` | +| `pnpm lint` | ESLint | +| `pnpm test` | unit-тесты | +| `pnpm test:api` | API-тесты PocketBase: схема, правила доступа, изоляция пользователей | +| `pnpm test:integration` | репозитории приложения против тестового PocketBase | +| `pnpm test:lint` | тесты конфигов ESLint и dev-прокси; первый шаг `test`, `test:api`, `test:integration` | +| `pnpm e2e` | сценарии в браузере и проверка доступности (axe) | +| `pnpm e2e:prod` | сценарии на production-сборке | +| `pnpm pb:serve` | PocketBase для разработки | +| `pnpm pb:migrate` | применить миграции | +| `pnpm pb:superuser` | создать суперпользователя или сбросить его пароль | Тесты, которым нужен PocketBase, сами поднимают временный экземпляр и не трогают `pocketbase/pb_data`. Перед первым запуском e2e установите браузер: `pnpm exec playwright install chromium`. diff --git a/angular.json b/angular.json index 548c2cb..7b3bb91 100644 --- a/angular.json +++ b/angular.json @@ -59,6 +59,9 @@ "defaultConfiguration": "production" }, "serve": { + "options": { + "proxyConfig": "proxy.conf.mjs" + }, "builder": "@angular/build:dev-server", "configurations": { "production": { diff --git a/docs/architecture.md b/docs/architecture.md index a070fd3..c7c0b75 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -174,7 +174,7 @@ PocketBase SDK использует `fetch`, а не `HttpClient`, поэтом - **Сервер:** `PB_URL` — переменная окружения Node-сервера, внутренний адрес PocketBase для серверного рендера (по умолчанию `http://127.0.0.1:8090`). В браузер не передаётся. - **Браузер:** `/pb`. Префикс `/pb` — константа `PB_PUBLIC_PATH` в `shared/pocketbase/`; если путь в nginx поменяется, меняется только она. - Всё это задаётся в одном месте: `shared/pocketbase/`. Клиент `POCKETBASE` создаётся одной и той же фабрикой и в браузере, и на сервере (`providePocketBase()` в `app.config.ts`); различается только токен `PB_URL`: `providePocketBase()` даёт браузерное значение по умолчанию, а `providePbUrlForServer()` в `app.config.server.ts` подменяет его серверным. -- **Разработка:** `ng serve` проксирует `/pb/*` → `PB_URL` со срезанием префикса (`proxy.conf.mjs`). E2E-тесты production-сборки используют маленький тестовый фронт-прокси, повторяющий правила nginx. +- **Разработка:** `ng serve` проксирует `/pb/*` → `PB_URL` со срезанием префикса (`proxy.conf.mjs`; ключ `/pb/` как у `location /pb/` в nginx, формат — Vite `server.proxy`: `target`, `changeOrigin`, `rewrite`). E2E-тесты production-сборки используют маленький тестовый фронт-прокси, повторяющий правила nginx. ### Ожидаемая конфигурация nginx (пример, деплой — вне MVP) diff --git a/docs/testing.md b/docs/testing.md index ddfa2fa..611a783 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -18,7 +18,7 @@ `pnpm test:api` = `tsc -p tsconfig.tests.json && vitest run --config vitest.api.config.mts`: чистый Vitest в Node, без Angular. -- Сначала проверяются типы тестового кода (`tsconfig.tests.json`: `tests/**` и `vitest.*.config.mts`): сам Vitest типы не проверяет. +- Сначала проверяются типы тестового кода (`tsconfig.tests.json`: `tests/**`, `vitest.*.config.mts` и `proxy.conf.mjs`; `allowJs` нужен, чтобы `tests/lint/` импортировали `.mjs`-конфиг): сам Vitest типы не проверяет. - `vitest.api.config.mts`: `include: tests/api/**/*.api.spec.ts`, `globalSetup: tests/support/global-setup.ts`, таймауты 20 с (запас на старт PocketBase внутри теста). - `global-setup.ts` поднимает один PocketBase на прогон и передаёт тестам `pbUrl`, `pbDataDir`, `pbSuperuser` через `project.provide`; в тестах — `inject('pbUrl')` и т. д. из `vitest`. Типы этих значений — `tests/support/provided-context.d.ts`. - Тесты самого стенда — `tests/api/harness.api.spec.ts`. Проверка уборки по `SIGINT`/`SIGTERM` запускает фикстуру `tests/support/fixtures/hold-pocketbase.ts` отдельным процессом `node`: она поднимает PocketBase, печатает `{ url, dataDir }` и ждёт сигнала. @@ -37,12 +37,13 @@ `tests/support/clients.ts` компилируется в двух программах — `tsconfig.tests.json` и `tsconfig.spec.json`. Типы Node подключены в обеих (спеки выполняются Vitest в Node, а код в `src/` вроде серверного провайдера `PB_URL` читает `process.env`), а `tests/support/provided-context.d.ts` с типами `inject('pbUrl')` перечислен в `include` обоих tsconfig: `references` в `tsconfig.json` программы не объединяют. -## Тесты конфига ESLint +## Тесты конфигов (ESLint, dev-прокси) `pnpm test:lint` = `tsc -p tsconfig.tests.json && vitest run --config vitest.lint.config.mts`: чистый Vitest в Node, файлы `tests/lint/**/*.spec.ts`. - Проверяют, что ESLint держит границы, от которых зависит изоляция данных при SSR: `new PocketBase(` и импорт класса `PocketBase` (в том числе под другим именем и через `pocketbase/*`) только в фабрике `shared/pocketbase/pocketbase-client.ts` (и в тестовых хелперах `tests/`), импорт `pocketbase` и `pocketbase/*` запрещён в `domain/` и `ui/`. -- Способ: `ESLint#lintText(code, { filePath })` на коротких фрагментах кода, проверяется `ruleId` сообщений. Конфиг ESLint ломается молча (опечатка в glob, слишком широкое исключение), и `pnpm lint` при этом остаётся зелёным; тест это ловит. +- `tests/lint/pb-dev-proxy.spec.ts` проверяет `proxy.conf.mjs` (dev-прокси `ng serve`): правило `/pb/`, `target` из `PB_URL` с тем же значением по умолчанию, что у серверного провайдера (в том числе при пустой переменной), и срезание префикса `/pb`. +- Способ ESLint-тестов: `ESLint#lintText(code, { filePath })` на коротких фрагментах кода, проверяется `ruleId` сообщений. Конфиг ESLint ломается молча (опечатка в glob, слишком широкое исключение), и `pnpm lint` при этом остаётся зелёным; тест это ловит. - Запускаются первым шагом в `pnpm test`, `pnpm test:api` и `pnpm test:integration`, чтобы поломка правила обнаружилась при любом прогоне. ESLint — Node-библиотека, в бандл builder'а Angular её не подключить, поэтому это отдельный набор. ## Контрактные тесты репозиториев diff --git a/proxy.conf.mjs b/proxy.conf.mjs new file mode 100644 index 0000000..8706c74 --- /dev/null +++ b/proxy.conf.mjs @@ -0,0 +1,11 @@ +// @ts-check +// Повторяет `location /pb/` из docs/architecture.md (раздел 7): только пути с префиксом `/pb/`, префикс срезается. +// Формат — `server.proxy` dev-сервера Vite (@angular/build). +/** @type {Record string }>} */ +export default { + '/pb/': { + target: process.env['PB_URL'] || 'http://127.0.0.1:8090', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/pb/, ''), + }, +}; diff --git a/tests/lint/pb-dev-proxy.spec.ts b/tests/lint/pb-dev-proxy.spec.ts new file mode 100644 index 0000000..3f6e050 --- /dev/null +++ b/tests/lint/pb-dev-proxy.spec.ts @@ -0,0 +1,36 @@ +// Проверяет правило `/pb/` в proxy.conf.mjs: оно должно повторять `location /pb/` из nginx и серверный `PB_URL`. +import { afterEach, describe, expect, it, vi } from 'vitest'; + +type Rules = typeof import('../../proxy.conf.mjs').default; + +async function loadRules(pbUrl: string | undefined): Promise { + vi.stubEnv('PB_URL', pbUrl); + vi.resetModules(); + return (await import('../../proxy.conf.mjs')).default; +} + +describe('прокси /pb для ng serve', () => { + afterEach(() => vi.unstubAllEnvs()); + + it('содержит правило /pb/ с target из PB_URL', async () => { + const rules = await loadRules('http://pb.test:1234'); + + expect(rules['/pb/']?.target).toBe('http://pb.test:1234'); + expect(rules['/pb/']?.changeOrigin).toBe(true); + }); + + it.each([undefined, ''])('при PB_URL=%j target — http://127.0.0.1:8090', async (pbUrl) => { + const rules = await loadRules(pbUrl); + + expect(rules['/pb/']?.target).toBe('http://127.0.0.1:8090'); + }); + + it('rewrite срезает префикс /pb', async () => { + const rewrite = (await loadRules(undefined))['/pb/']?.rewrite; + + expect(rewrite?.('/pb/api/health')).toBe('/api/health'); + expect(rewrite?.('/pb/api/collections/x/records?a=1')).toBe( + '/api/collections/x/records?a=1', + ); + }); +}); diff --git a/tsconfig.tests.json b/tsconfig.tests.json index e5bd54a..9790096 100644 --- a/tsconfig.tests.json +++ b/tsconfig.tests.json @@ -5,6 +5,7 @@ "strict": true, "noEmit": true, "allowImportingTsExtensions": true, + "allowJs": true, "types": ["node"] }, "files": [], @@ -12,6 +13,7 @@ "tests/**/*.ts", "vitest.api.config.mts", "vitest.integration.config.mts", - "vitest.lint.config.mts" + "vitest.lint.config.mts", + "proxy.conf.mjs" ] }