mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Merge pull request #692 from fsolla/OPS40-worktree-dev-port-do-env-local-nao-e-honrado-pelo-next
fix(OPS40): pnpm dev honra o PORT do .env.local (next dev -p via scripts/dev.mjs)
This commit is contained in:
commit
3ac5dbe0bd
7 changed files with 219 additions and 2 deletions
|
|
@ -30,6 +30,7 @@ These are product/architecture constraints — not a ban on editing existing cod
|
|||
**Database & local development (safety-critical — production is a live Neon Postgres with real citizens' PII):**
|
||||
|
||||
- Local dev and tests must NEVER touch production. Local dev runs against a local Docker Postgres (`pnpm db:start`); `pnpm dev` refuses a non-local `DATABASE_URL` (guard: `scripts/guard-dev-db.mjs`) unless `ALLOW_REMOTE_DB=true`. Tests load `.env.test` (`teqo_test`) and refuse any database whose name doesn't end in `_test` (`tests/helpers/assertTestDatabase.ts`). Never repoint `.env`/`.env.local`/`.env.test` `DATABASE_URL` at Neon. Full workflow lives in the `local-database` skill (`.agents/skills/local-database`).
|
||||
- **`pnpm dev` honra o `PORT` do `.env.local` (OPS40):** `next dev` resolve a porta ANTES de `@next/env` carregar os env-files (commander `.env('PORT')` no `bin/next`), então o `PORT=3100+slot` do provisionador de worktree era ignorado e todo dev server subia em 3000. `pnpm dev` hoje passa por `scripts/dev.mjs`: roda o mesmo preflight do `guard-dev-db`, carrega `.env.local` → `.env` (`override:false` — env real do processo vence, como o `PORT` que o webServer do Playwright injeta) e entrega a porta ao `next dev` como **flag CLI `-p`** (`resolveDevPort`/`nextDevArgs` em `scripts/lib/cli.mjs`, unit-testados); sem `PORT` em lugar nenhum, nenhum `-p` é passado e o default 3000 (com `allowRetry`) do Next segue intacto no main repo.
|
||||
- Schema changes go through committed Payload migrations — `push: false` everywhere, never flip it on against a remote DB. Edit configs → `pnpm migrate:create <name>` → commit `src/migrations/*` (both `.ts` and `.json`, plus `index.ts`) → `pnpm migrate` locally. `pnpm build` runs `payload migrate`, so **every Vercel deploy applies pending migrations to prod**. Prod is baselined at migration `20260715_163458_initial` — never regenerate or replace the initial migration. Full workflow (incl. hand-written data/reconciliation migrations) lives in the `payload-migrations` skill (`.agents/skills/payload-migrations`).
|
||||
- `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit.
|
||||
- To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data).
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
---
|
||||
|
||||
**Recently resolved (2026-08-11):** **Plan-issue — rascunho UI/UX do gate vira HTML+Tailwind commitado no repo (ad-hoc, decisão do usuário).** O artefato deixa de ser **Cursor canvas** (`~/.cursor/projects/<workspace>/canvases/plan-<id>-ui-draft.canvas.tsx`, fora do repo, só renderizava no IDE de quem planejou e apodrecia) e passa a ser **HTML + Tailwind v4** (`docs/plans/<slug>-ui-draft.html` — `@tailwindcss/browser@4` via jsdelivr, mesma major do app 4.1.x; rede só na hora de renderizar) **commitado com o plano** e **renderizado em PNG** por `pnpm ui-draft:render` (`scripts/render-ui-draft.mjs`, Chromium do Playwright via `@playwright/test`, fallback `load` se o CDN não settle — nunca toca banco/app): uma PNG por cena `<section data-shot="<rótulo>">` (ex. `mobile`, `desktop-vazio`) → `docs/plans/<slug>-ui-draft-<rótulo>.png`, ou full-page sem `data-shot`; PNGs **embutidos no plano** (visíveis no chat do gate E no PR — o aceite vira registro permanente; o HTML é a fonte iterável). **Contrato de fidelidade explícito em `ui-draft-html.md`** (substitui `ui-draft-canvas.md`, deletado; refs atualizadas em SKILL.md/frontmatter, intention-template.md — campo renomeado `Canvas UI:` → `Rascunho UI:` + secção de embed, skills-map.md, shaping.md): o rascunho DEVE ter layout real, espaçamentos/tamanhos na escala do app (`p-4`, `gap-3`, `h-11`, `text-sm`), breakpoints reais (390/1280) e estados críticos como cenas estáticas; é PROIBIDO ser funcional (zero JS de comportamento — cada estado é cena separada), usar código de app (`src/`), brand/polish (gradientes/emojis/sombras/logo/paleta do produto — cinza neutro + um acento), mock de dados realistas ou decisão de engenharia. Gate: iterar HTML → re-render → embutir até confirmação explícita. Verificado: render real do skeleton (classes v4 aplicadas — oklch zinc, rounded-lg 8px, text-lg 18px). Sem migration/access/Consent/UI de produto/schema. Sem issue — ad-hoc no worktree `work/5`.
|
||||
**Recently resolved (2026-08-11):** **OPS40** (Issue #682) — `pnpm dev` honra o `PORT` do `.env.local`: `next dev` resolve a porta **antes** de `@next/env` carregar env-files (commander `.env('PORT')` no `bin/next` — causa confirmada no fonte 15.4.11), então o `PORT=3100+slot` do provisionador de worktree era ignorado e todo dev server subia em 3000 (colisão entre worktrees paralelos; o e2e escapava porque o webServer injeta `PORT` no env real). **`pnpm dev`/`devsafe` agora passam por `scripts/dev.mjs`** (wrapper): importa o `guard-dev-db.mjs` (preflight fail-closed inline, mesma saída), `loadCliEnv()` (`.env.local` → `.env`, `override:false`) e entrega a porta ao `next dev` como **flag CLI `-p`** — a única alavanca que o repo controla. Precedência: env real do processo (webServer do Playwright manda) → `.env.local` (worktree) → `.env` → sem `-p` (default 3000 + `allowRetry` do Next intactos no main repo). Derivação pura unit-testada em `scripts/lib/cli.mjs` (`resolveDevPort` — null quando ausente, throw fail-closed em PORT inválida; `nextDevArgs`); spawn `node next/dist/bin/next` com `NODE_OPTIONS=--no-deprecation` (paridade cross-env), stdio inherit, forward SIGINT/SIGTERM/SIGHUP, propagação de exit code; extras (`--turbopack`) repassados após o `-p` derivado. Sem migration/access/Consent/UI. Verificação: boot manual no worktree → "Local: http://localhost:3140" (antes 3000), unit 8/8 novos, e2e dev-mode `campaignAuth` 3/3 (2 runs de prewarm estouraram o orçamento sob load ~50+ da máquina — flake de contenção fichado B17/B27/B32/B42, terceira run verde com cache quente). Nota fora de escopo: `AGENTS.md` em `main` carrega marcadores de conflito commitados (OPS33/OPS37) — área de main-mess da OPS38; o novo bullet do Database section foi adicionado **fora** da região conflitada. Intenção [`worktree-dev-port-nao-honrado.md`](plans/worktree-dev-port-nao-honrado.md); impl [`worktree-dev-port-nao-honrado-impl.md`](plans/worktree-dev-port-nao-honrado-impl.md).
|
||||
|
||||
**Recently resolved (2026-08-11):** **C124** (Issue #668) — tarefa de atividade fica só com **texto, responsável e concluída**: o campo `due` (Prazo, datetime) sai do modelo e da UI. Remoção em cadeia num único commit simétrico: campo do array `tasks` da `activity` (o guardrail de liderança perde a comparação de `due` no mesmo item — continua fail-closed sobre título/responsável/contagem), `taskSchema` zod, `parseTasksFormData`/`ParsedActivityTask` (formData), `ActivityFormTaskViewModel`/`ActivityTaskViewModel`, input `datetime-local` do `ActivityTaskFields` (grid `2fr_1fr`) e linha "Prazo: …" do `ActivityTaskChecklist` (subtítulo condicional + `break-words` no título — P2 dos reviewers). `doneAt` ("concluída em") permanece como derivado invisível; `taskTotal`/`taskDoneCount` intocados. **Migration** `20260811_183016_drop_activity_task_due` (gerada; diff do gerador tinha no-op de `google_calendar_sync` por snapshot stale — limpa à mão para só o `DROP COLUMN "due"` de `activity_tasks`; dados descartados conforme decidido no gate). Grep pós: zero `due` fora das migrations (histórico frozen); nenhum consumidor fora da vertical (notificações, Google sync, quick actions). Gates: unit+int 736/736, e2e atividade 9/9 (1 flake ECONNRESET no run paralelo — rerun limpo), build OK. Intenção [`atividades-tarefas-sem-prazo.md`](plans/atividades-tarefas-sem-prazo.md); impl [`atividades-tarefas-sem-prazo-impl.md`](plans/atividades-tarefas-sem-prazo-impl.md).
|
||||
**Recently resolved (2026-08-11):** **C118** (Issue #657) — página de detalhe da pessoa `/campanha/pessoas/[id]` (staff), montada pelas **capacidades** da ficha: a mesma rota serve liderança, dobradinha, assessora, apoiador — e só mostra o que a pessoa tem. **Loader irmão** `loadPersonDetail` (`src/utilities/people/personDetail.ts`) reusa o merge da lista (`mergePeopleSources` + `scopePeopleRows`) com os mesmos selects por fonte — o **escopo do assessor é literalmente o da lista** (pessoa só aparece se alguma capacidade municipal estiver na carteira; `null` → `notFound()`), staff com bypass justificado (identity-gated, prova pelo merge); **seção Apoiador** (dado novo vs. view model da lista) lê `supporter` por contact com o access da própria collection (`overrideAccess: false`) — camada dupla, nunca alarga (int testado: apoiador de município fora da carteira é cortado). **`[id]` é `contactID`** (a chave de linha da C100 — e o link do Nome na lista, que a C116 nasceu apontando para `/campanha/liderancas/[id]`, agora aponta para cá). UI: `PersonDetailSections` em `components/campaign/people/` — Ficha (partido/telefone/e-mail/base + pills de capacidade), Liderança (status, acesso ao app, municípios, link "Abrir detalhe de liderança" — convivência v1 com `/campanha/liderancas/[id]`, votos continuam lá), Dobradinha, Assessora (por conta, `campaignRoleLabels`), Assessorado, Apoiador (fonte · município · intenção quando consentida · data), Ações reusando os botões da lista (WhatsApp, `LeadershipInviteRowAction` só com liderança, `DeletePersonButton` ganhou `deletedHref` opcional → volta à lista pós-delete). Sem migration/access novo/Consent; sem votos/KPI (intenção corta; assimetria intacta). Testes: int `personDetail` (matriz de escopo: unrestricted completo, assessor na carteira vê, fora → null, corte de supporter, só-staff só para unrestricted, conta leader não acende Assessora, contato solto/leader → null) e e2e novos em `campaignPeople` (coordinator vê seções por capacidade + link de liderança; dobradinha+apoiador sem bloco de liderança; leader → `/campanha/contatos`). Verificado ao vivo em dev (coordenador/assessor/leader, 404 de id inválido, mobile). Intenção [`pessoas-detalhe-por-capacidades.md`](plans/pessoas-detalhe-por-capacidades.md); impl [`pessoas-detalhe-por-capacidades-impl.md`](plans/pessoas-detalhe-por-capacidades-impl.md).
|
||||
|
|
|
|||
75
docs/plans/worktree-dev-port-nao-honrado-impl.md
Normal file
75
docs/plans/worktree-dev-port-nao-honrado-impl.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Impl: worktree dev — `PORT` do `.env.local` honrado pelo `pnpm dev`
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-11
|
||||
Issue: #682 (OPS40)
|
||||
Intenção: docs/plans/worktree-dev-port-nao-honrado.md
|
||||
Appetite restante: herdado (~2–4h; a entrega é pequena, sobra verificação)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** `pnpm dev` num worktree provisionado sobe em `3100+slot` (a porta do `.env.local`); dois worktrees em `pnpm dev` simultâneos não colidem; e2e (webServer com `PORT` explícito) e main repo (`pnpm dev` → 3000) intactos; ninguém precisa lembrar `-p`.
|
||||
- **O que NÃO negociar:** env do processo do webServer do Playwright continua sendo a fonte que manda no e2e (injeção explícita de `PORT`); main repo sem `PORT` em lugar nenhum continua em 3000; guard de DB local (fail-closed) continua rodando antes do dev server.
|
||||
- **O que reavaliar:** a hipótese "o problema é o `next dev` ignorar env-file" está **confirmada e com causa exata** — o CLI do Next 15.4.11 resolve a porta via `commander` `.option('-p, --port').env('PORT')` em `next/dist/bin/next` (linha 92), **antes** de `@next/env` carregar `.env.local`; o `env('PORT')` do commander só enxerga o env real do processo. A porta precisa ir como **flag CLI** `-p`. O wrapper do candidato (a) é o caminho certo; refinei os detalhes (guard inline, helpers puros unit-testados, spawn direto do bin do Next).
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
dev["pnpm dev"] --> wrap["node scripts/dev.mjs"]
|
||||
wrap --> guard["import scripts/guard-dev-db.mjs<br/>(preflight DB local, fail-closed)"]
|
||||
wrap --> env["loadCliEnv()<br/>.env.local › .env, override:false"]
|
||||
env --> port["resolveDevPort(process.env)<br/>real env › .env.local › .env › default"]
|
||||
port --> spawn["spawn node next/dist/bin/next<br/>dev [-p N] (stdio inherit)"]
|
||||
spawn --> next["next dev na porta certa<br/>(flag CLI ganha do env)"]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A (wrapper `scripts/dev.mjs`) | B (`next.config` `port`) | C (shell one-liner com `grep` no `.env.local`) | D (upstream Next)
|
||||
**Recomendação:** A — causa raiz é o parse da porta antes do env-file; a única alavanca que o repo controla é entregar a porta como flag CLI. O wrapper é um ponto único, testável e não exige que ninguém lembre de nada.
|
||||
**Rejeitadas:**
|
||||
|
||||
- **B** — `next.config` também é avaliado dentro do mesmo bootstrap que já ignorou o env-file para a porta; `port` no config lê `process.env` na mesma ordem problemática (e nem existe `port` de dev no config do Next 15.4 que valha). Mesma classe de bug, sem ganho de evidência.
|
||||
- **C** — duplica parsing de `.env.local` (semântica de aspas/comentários do dotenv), quebra precedência real-env-vs-arquivo, frágil no shell.
|
||||
- **D** — upstream, fora de controle; o wrapper sobrevive a mudanças do Next (se um dia `PORT` de env-file for honrado, o wrapper vira no-op correta).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`resolveDevPort` / `nextDevArgs`** (`scripts/lib/cli.mjs`, novos exports puros): `resolveDevPort(env)` → `number | null` (`null` = sem `-p`, Next decide o default 3000; inválido não-vazio → `Error` fail-closed, ex.: `PORT=abc`); `nextDevArgs(port)` → `['dev']` ou `['dev', '-p', String(port)]`. Reusa o padrão de "derivação pura unit-testada" de `scripts/lib/worktree-env.mjs`.
|
||||
- **`scripts/dev.mjs`** (novo, wrapper): importa `guard-dev-db.mjs` (o preflight roda inline; `process.exit(1)` do guard derruba tudo — mesmo fail-closed de hoje), chama `loadCliEnv()`, resolve a porta, e dá `spawn(process.execPath, [require.resolve('next/dist/bin/next'), ...nextDevArgs(port), ...extras], { stdio: 'inherit', env: { ...process.env, NODE_OPTIONS: '--no-deprecation' } })` — paridade exata com o `cross-env NODE_OPTIONS=--no-deprecation` atual. Forward de SIGINT/SIGTERM/SIGHUP + propagação de exit code. `process.argv.slice(2)` passa extras (ex.: `--turbopack`) depois do `-p` derivado (último vence no parser do commander; contrato preservado).
|
||||
- **`package.json`**: `dev` → `node scripts/dev.mjs`; `devsafe` → `rm -rf .next && node scripts/dev.mjs`. `cross-env` continua em uso por build/lint/etc.
|
||||
- **Migration:** sem migration (nenhum schema).
|
||||
- **Access / Consent:** N/A.
|
||||
- **UI:** N/A (Impeccable A).
|
||||
|
||||
### Precedência da porta (contrato)
|
||||
|
||||
1. `process.env.PORT` real (webServer do Playwright injeta — e2e continua dono da porta);
|
||||
2. `PORT` do `.env.local` (provisionador do worktree);
|
||||
3. `PORT` do `.env` (sem uso hoje, mas simétrico);
|
||||
4. nenhum → sem `-p` → Next default 3000 (main repo intacto; `allowRetry` do Next continua ativo só neste caso — comportamento atual preservado).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Helpers puros** — `resolveDevPort`/`nextDevArgs` em `scripts/lib/cli.mjs` + `tests/unit/devPort.unit.spec.ts` (casos: ausente→null, `3140`→3140, espaço em volta→trim, `0`/`abc`/`65536`→throw, `65535`→ok; args com/sem `-p`). `pnpm test:unit`.
|
||||
2. **Wrapper + scripts** — `scripts/dev.mjs` + package.json. Verificação manual: `pnpm dev` neste worktree (`.env.local` `PORT=3140`) → log "Local: http://localhost:3140"; e2e dev-mode smoke (`pnpm test:e2e` com `--grep` curto) provando o caminho webServer→wrapper→porta certa.
|
||||
3. **Docs + gates** — AGENTS.md (menção ao wrapper no contrato de worktrees + no bullet do guard), entrada curta no `docs/CHANGELOG-AGENTS.md`, status do plano de intenção. Gates: `gate:fast` + `format:check` + `knip` + `check:cycles`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Não carregar `.env.local` duas vezes com ordens conflitantes — `loadCliEnv` já é idempotente (`override:false`); o wrapper usa a MESMA fonte do provisionador.
|
||||
- Não dar prioridade ao `.env.local` sobre o env real — e2e perderia a porta forçada (rabbit hole apontado na intenção; a precedência acima o elimina estruturalmente).
|
||||
- Não mexer no guard (`guard-dev-db.mjs` continua standalone e é quem decide o DB).
|
||||
- Fora de escopo: `next start`/`pnpm start` (e2e prod-mode usa 3000 e `PORT` real — intocado), re-provisionamento de worktrees existentes, e os marcadores de conflito não resolvidos no `AGENTS.md` em `main` (pré-existentes — ver Riscos).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Colisão de porta real (EADDRINUSE) vira erro alto em vez de retry silencioso** — desejado: worktree colidindo deve falhar alto com o número da porta, não migrar sozinho (contrato `3100+slot`). `allowRetry` do Next só ativa no source `default` (sem `-p`), exatamente o caso do main repo. Mitigação: mensagem de erro clara do Next basta.
|
||||
- **`AGENTS.md` em main tem conflito commitado (OPS33/OPS37)**: não é deste escopo e há Issue de main-mess (OPS38) em aberto para a área — registrar como débito no capture-review-debts, não resolver aqui.
|
||||
- **NODE_OPTIONS** — paridade com o cross-env atual (substitui, não concatena).
|
||||
- **Windows** — não é alvo do repo; `spawn` via `process.execPath` + bin do Next é neutro de plataforma de qualquer forma.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto (porta do worktree honrada; e2e e main intactos)
|
||||
- [x] Invariantes AGENTS/engineering-standards (guard fail-closed preservado; helpers puros unit-testados)
|
||||
- [x] Testes de domínio previstos (unit `devPort.unit.spec.ts`; verificação manual do caminho real)
|
||||
|
|
@ -44,8 +44,8 @@
|
|||
"build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs",
|
||||
"build:municipality-zone-geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-municipality-zone-geometries.mjs",
|
||||
"build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs",
|
||||
"dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"devsafe": "rm -rf .next && node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"dev": "node scripts/dev.mjs",
|
||||
"devsafe": "rm -rf .next && node scripts/dev.mjs",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"gate:fast": "pnpm lint && pnpm typecheck && pnpm test:unit",
|
||||
|
|
|
|||
64
scripts/dev.mjs
Normal file
64
scripts/dev.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* `pnpm dev` launcher (OPS40). `next dev` resolves its port from the `-p` flag
|
||||
* or the REAL process env BEFORE `@next/env` loads `.env.local`, so a worktree
|
||||
* provisioner's `PORT=3100+slot` was silently ignored and every worktree bound
|
||||
* 3000. The wrapper loads the env files itself (same precedence as the guard),
|
||||
* hands the port to Next as a CLI flag, and spawns the dev server in-process.
|
||||
*
|
||||
* Precedence (exactly what `loadCliEnv` merges, override:false):
|
||||
* 1. real `process.env.PORT` (Playwright's webServer injects it — e2e wins)
|
||||
* 2. `.env.local` (the worktree provisioner)
|
||||
* 3. `.env`
|
||||
* 4. none → no `-p` → Next default 3000 + its free-port retry (main repo)
|
||||
*/
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
import './guard-dev-db.mjs'
|
||||
import { dieWithLabel, loadCliEnv, nextDevArgs, resolveDevPort } from './lib/cli.mjs'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const die = dieWithLabel('dev')
|
||||
|
||||
loadCliEnv()
|
||||
|
||||
let port
|
||||
try {
|
||||
port = resolveDevPort(process.env)
|
||||
} catch (error) {
|
||||
die(error.message)
|
||||
}
|
||||
|
||||
const extraArgs = process.argv.slice(2)
|
||||
const nextArgs = [...nextDevArgs(port), ...extraArgs]
|
||||
const nextBin = require.resolve('next/dist/bin/next')
|
||||
|
||||
if (port === null) {
|
||||
console.log(`[dev] next dev ${nextArgs.join(' ')} (sem PORT — porta padrão 3000)`)
|
||||
} else {
|
||||
console.log(`[dev] dev server na porta ${port} (PORT do env) — next dev -p ${port}`)
|
||||
}
|
||||
|
||||
const child = spawn(process.execPath, [nextBin, ...nextArgs], {
|
||||
stdio: 'inherit',
|
||||
// Paridade com o cross-env NODE_OPTIONS=--no-deprecation anterior.
|
||||
env: { ...process.env, NODE_OPTIONS: '--no-deprecation' },
|
||||
})
|
||||
|
||||
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
||||
process.on(signal, () => child.kill(signal))
|
||||
}
|
||||
|
||||
child.on('error', (error) => die(`falha ao subir o next dev: ${error.message}`))
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
// O listener acima re-forwardaria o sinal para um filho já morto e o
|
||||
// kill(process.pid) re-dispararia este próprio handler — remove antes de
|
||||
// re-sinalizar para deixar a disposição padrão encerrar o processo.
|
||||
process.removeAllListeners(signal)
|
||||
process.kill(process.pid, signal)
|
||||
} else {
|
||||
process.exit(code ?? 1)
|
||||
}
|
||||
})
|
||||
|
|
@ -36,6 +36,36 @@ export const ALLOW_REMOTE_DB_FLAG = 'ALLOW_REMOTE_DB'
|
|||
export const isRemoteDbOverrideSet = () =>
|
||||
process.env[ALLOW_REMOTE_DB_FLAG] === 'true' || process.env[ALLOW_REMOTE_DB_FLAG] === '1'
|
||||
|
||||
/**
|
||||
* Port for `next dev` (OPS40). Next's CLI resolves its port via commander's
|
||||
* `.env('PORT')` BEFORE `@next/env` loads `.env.local`, so a `PORT` written by
|
||||
* the worktree provisioner was silently ignored and every `pnpm dev` bound
|
||||
* 3000. `loadCliEnv()` has already merged real env > `.env.local` > `.env`
|
||||
* (override:false) into `process.env`, so reading `env.PORT` gives exactly the
|
||||
* precedence the repo wants: real env (Playwright's webServer) wins, then the
|
||||
* worktree's file, then no port at all (Next's 3000 default + allowRetry).
|
||||
* Returns null when absent (caller omits `-p`); throws fail-closed on a
|
||||
* non-empty invalid value (PORT=abc must not silently fall back to 3000).
|
||||
*
|
||||
* @param {Record<string, string | undefined>} [env]
|
||||
*/
|
||||
export const resolveDevPort = (env = process.env) => {
|
||||
const raw = (env.PORT ?? '').trim()
|
||||
if (raw === '') return null
|
||||
const port = Number(raw)
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error(`PORT inválida: ${JSON.stringify(raw)} (esperado 1..65535)`)
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
/**
|
||||
* `next dev` argv for a resolved port. The port MUST go as a CLI flag — the
|
||||
* env-file value never reaches commander's `env('PORT')`. A null port omits
|
||||
* `-p`, keeping Next's default 3000 and its "retry a free port" behavior.
|
||||
*/
|
||||
export const nextDevArgs = (port) => (port === null ? ['dev'] : ['dev', '-p', String(port)])
|
||||
|
||||
/**
|
||||
* download → cache → sha256 with provenance logging. `ext` picks the cache
|
||||
* filename; when it is 'json' the parsed body rides along. `expectedSha256`
|
||||
|
|
|
|||
46
tests/unit/devPort.unit.spec.ts
Normal file
46
tests/unit/devPort.unit.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { nextDevArgs, resolveDevPort } from '../../scripts/lib/cli.mjs'
|
||||
|
||||
describe('resolveDevPort (OPS40 — next dev ignores env-file PORT)', () => {
|
||||
it('no PORT anywhere → null (caller omits -p, Next defaults to 3000)', () => {
|
||||
expect(resolveDevPort({})).toBeNull()
|
||||
expect(resolveDevPort({ DATABASE_URL: 'x' })).toBeNull()
|
||||
})
|
||||
|
||||
it('an empty/whitespace PORT is treated as absent', () => {
|
||||
expect(resolveDevPort({ PORT: '' })).toBeNull()
|
||||
expect(resolveDevPort({ PORT: ' ' })).toBeNull()
|
||||
})
|
||||
|
||||
it('reads the worktree PORT from the merged env (.env.local)', () => {
|
||||
expect(resolveDevPort({ PORT: '3140' })).toBe(3140)
|
||||
})
|
||||
|
||||
it('trims whitespace around the value', () => {
|
||||
expect(resolveDevPort({ PORT: ' 3140 ' })).toBe(3140)
|
||||
})
|
||||
|
||||
it('accepts the port range bounds', () => {
|
||||
expect(resolveDevPort({ PORT: '1' })).toBe(1)
|
||||
expect(resolveDevPort({ PORT: '65535' })).toBe(65535)
|
||||
})
|
||||
|
||||
it('fails closed on a non-empty invalid PORT instead of falling back to 3000', () => {
|
||||
for (const bad of ['abc', '0', '-1', '65536', '31.5', '12a']) {
|
||||
expect(() => resolveDevPort({ PORT: bad })).toThrow(/PORT inválida/)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('nextDevArgs (port must go as a CLI flag — commander never sees env files)', () => {
|
||||
it('passes -p when a port is resolved', () => {
|
||||
expect(nextDevArgs(3140)).toEqual(['dev', '-p', '3140'])
|
||||
})
|
||||
|
||||
it('omits -p when there is no port (Next default 3000 + allowRetry)', () => {
|
||||
expect(nextDevArgs(null)).toEqual(['dev'])
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue