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 <noreply@anthropic.com>
This commit is contained in:
kamask
2026-10-31 20:24:13 +03:00
co-authored by Claude Sonnet 5.5
parent 2852610488
commit bf72af9ef4
8 changed files with 73 additions and 19 deletions
+1
View File
@@ -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=
+2 -2
View File
@@ -48,14 +48,14 @@ 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 test:lint` | тесты конфигов ESLint и dev-прокси; первый шаг `test`, `test:api`, `test:integration` |
| `pnpm e2e` | сценарии в браузере и проверка доступности (axe) |
| `pnpm e2e:prod` | сценарии на production-сборке |
| `pnpm pb:serve` | PocketBase для разработки |
+3
View File
@@ -59,6 +59,9 @@
"defaultConfiguration": "production"
},
"serve": {
"options": {
"proxyConfig": "proxy.conf.mjs"
},
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
+1 -1
View File
@@ -174,7 +174,7 @@ PocketBase SDK использует `fetch`, а не `HttpClient`, поэтом
- **Сервер:** `PB_URL` — переменная окружения Node-сервера, внутренний адрес PocketBase для серверного рендера (по умолчанию `http://127.0.0.1:8090`). В браузер не передаётся.
- **Браузер:** `<window.location.origin>/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)
+4 -3
View File
@@ -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 её не подключить, поэтому это отдельный набор.
## Контрактные тесты репозиториев
+11
View File
@@ -0,0 +1,11 @@
// @ts-check
// Повторяет `location /pb/` из docs/architecture.md (раздел 7): только пути с префиксом `/pb/`, префикс срезается.
// Формат — `server.proxy` dev-сервера Vite (@angular/build).
/** @type {Record<string, { target: string; changeOrigin?: boolean; rewrite?: (path: string) => string }>} */
export default {
'/pb/': {
target: process.env['PB_URL'] || 'http://127.0.0.1:8090',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/pb/, ''),
},
};
+36
View File
@@ -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<Rules> {
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',
);
});
});
+3 -1
View File
@@ -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"
]
}