mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
fix(admin): restaura S3ClientUploadHandler no importMap — admin em branco em prod (4ª recorrência da classe OPS69)
O importMap commitado perdeu a entrada do handler S3 no 1c865bd8
(regenerado sem as envs S3_*): com S3_* em prod, getFromImportMap
retorna undefined e a raiz do Payload não monta — tela branca, zero
erros de console, invisível no loop local (storage local em dev/test).
Fix da causa: regeneração com S3_* dummy (+2 linhas). Prevenção no
mesmo PR — o guard planejado no OPS69 (depends #77/#87) que nunca
existiu: pin unit, guard CI+deploy que regenera com envs dummy e falha
em divergência, wrapper do generate:importmap que injeta as envs, e
contrato compartilhado da chave. Post-mortem + changelog incluídos.
This commit is contained in:
parent
8dedf1028c
commit
fea6c64514
11 changed files with 294 additions and 2 deletions
2
.github/workflows/ci-pr.yml
vendored
2
.github/workflows/ci-pr.yml
vendored
|
|
@ -132,6 +132,8 @@ jobs:
|
|||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
run: node scripts/check-plans-only-pr-closes.mjs
|
||||
- name: ImportMap keeps the S3 upload handler
|
||||
run: node scripts/check-importmap-s3.mjs
|
||||
|
||||
- name: Check test file locations
|
||||
run: node scripts/check-test-locations.mjs
|
||||
|
|
|
|||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
|
@ -73,6 +73,8 @@ jobs:
|
|||
|
||||
- name: Check test file locations
|
||||
run: node scripts/check-test-locations.mjs
|
||||
- name: ImportMap keeps the S3 upload handler
|
||||
run: node scripts/check-importmap-s3.mjs
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
- name: Format check
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ These are product/architecture constraints — not a ban on editing existing cod
|
|||
### Code Validation
|
||||
|
||||
- To validate typescript correctness after modifying code run `tsc --noEmit`
|
||||
- Generate import maps after creating or modifying components.
|
||||
- Generate import maps after creating or modifying components. **Always set the 4 `S3_*` envs (dummy values ok) when running `pnpm generate:importmap`** — without them the generator orphans `@payloadcms/storage-s3/client#S3ClientUploadHandler` and the admin goes blank in production (class OPS69/OPS72/OPS73; `scripts/check-importmap-s3.mjs` + the `importMapS3UploadHandler` unit pin guard this in CI).
|
||||
- Any change to a collection/global/field schema requires a migration: `pnpm migrate:create <name>`, then `pnpm migrate` locally. Never rely on `push` (it is `false`). See the `payload-migrations` skill.
|
||||
|
||||
## Project Structure
|
||||
|
|
|
|||
1
docs/changelog/2026-08-26-admin-branco-importmap-s3.md
Normal file
1
docs/changelog/2026-08-26-admin-branco-importmap-s3.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**Admin em branco — importMap órfão do handler S3, 4ª recorrência (2026-08-26):** o admin do Payload em produção (jorgesolla1313.com.br/admin) abria em TELA BRANCA (login e dashboard) — zero erros de console no browser, SSR 200, assets 200; detecção por relato humano. Causa: o `src/app/(payload)/admin/importMap.js` commitado perdeu a entrada `@payloadcms/storage-s3/client#S3ClientUploadHandler` no commit 1c865bd8 (2026-08-23, refactor "E1 simplify"), que regenerou o importMap sem as envs `S3_*`; com as envs setadas em prod (plugin s3Storage condicional em src/payload.config.ts, resolvedor src/utilities/mediaStorage.ts), `getFromImportMap` retorna undefined e a raiz do Payload não monta — o erro sai como console.error no log do SERVIDOR, invisível no browser; dev/test/CI sem `S3_*` usam storage local e o importMap órfão é invisível no loop local (e2e do admin passa), e o smoke do deploy só checa HTTP 200. Classe recorrente: OPS69/OPS72/OPS73 (4ª vez); o guard de CI planejado nas lições do OPS69 (depends #77/#87) nunca foi implementado. Fix causa: regeneração do importMap com `S3_*` dummy → diff de exatamente +2 linhas (import + entrada); sem migration/access/UI. Prevenção implementada no mesmo PR: pin unit `tests/unit/importMapS3UploadHandler.unit.spec.ts` (vermelho sem o fix, verde com); guard `scripts/check-importmap-s3.mjs` (regenera com envs dummy, exit 1 com drift + restore se divergir) ligado no ci-pr.yml (content guards) e no job verify do deploy.yml; wrapper `scripts/generate-importmap.mjs` no `pnpm generate:importmap` (injeta `S3_*` dummy quando ausentes — regeneração local nunca mais orfa o handler); contrato compartilhado `scripts/lib/importMapContract.mjs`; AGENTS.md atualizado. Verificação (prova do verificador 6/6): regressão e guard vermelho/verde provados, unit completa 284 arquivos/2682 testes verdes, e2e original com S3\_\* dummy (PORT 4093) monta login+dashboard com 0 `getFromImportMap` e 0 erros de console, tsc/lint exit 0, gate:fast verde. Prevenção documentada (não implementada): gerar o importMap no build (BuildKit secrets) em vez de commitá-lo, s3Storage incondicional no config, smoke de deploy headless que asserta o mount da raiz Payload, e fail-fast estruturado quando getFromImportMap retorna undefined. Deploy PENDENTE (manual, workflow_dispatch do deploy.yml) + confirmação do humano em prod. Post-mortem: `docs/postmortems/2026-08-26-admin-branco-importmap-s3.md`.
|
||||
81
docs/postmortems/2026-08-26-admin-branco-importmap-s3.md
Normal file
81
docs/postmortems/2026-08-26-admin-branco-importmap-s3.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Post-mortem: admin do Payload em tela branca — importMap órfão do handler S3
|
||||
|
||||
> Template do `/bug-fix`. Preencha com fatos apurados; o que não for apurado fica "não apurado" — nunca invente.
|
||||
|
||||
## Registro
|
||||
|
||||
| Campo | Valor |
|
||||
| ------------------- | ---------------- |
|
||||
| Data do post-mortem | 2026-08-26 |
|
||||
| Severidade | crítica |
|
||||
| Ambiente | prod |
|
||||
| Issue(s) | sem Issue |
|
||||
| PR do fix | — (em andamento) |
|
||||
| Detectado por | humano |
|
||||
|
||||
## Timeline
|
||||
|
||||
| Momento | Data/hora | Evento |
|
||||
| ------------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Início provável | 2026-08-23 | commit 1c865bd8 (refactor "E1 simplify — pin loading/retry/stale-quirk no spec e tipo local") regenerou o importMap sem as envs S3\_\*, removendo a entrada do handler |
|
||||
| Detecção | 2026-08-26 | relato do usuário: admin em branco em produção |
|
||||
| Correção mergeada | em andamento | PR ainda sem número |
|
||||
| Deploy | ainda não feito | dispatch manual do deploy.yml (só humano) |
|
||||
| Verificado em prod | não | aguarda deploy + confirmação do humano |
|
||||
|
||||
## O bug
|
||||
|
||||
O admin do Payload em produção (jorgesolla1313.com.br/admin) abria em TELA BRANCA — tanto no login quanto no dashboard. Zero erros de console no browser, SSR HTML completo (200) e todos os assets 200, então não havia nenhum sinal visível no cliente. **Sintoma — não a causa.**
|
||||
|
||||
## Causa-raiz
|
||||
|
||||
O `src/app/(payload)/admin/importMap.js` commitado perdeu a entrada `@payloadcms/storage-s3/client#S3ClientUploadHandler` — removida no commit 1c865bd8 (2026-08-23, refactor "E1 simplify — pin loading/retry/stale-quirk no spec e tipo local"), que regenerou o importMap SEM as envs S3\_\*.
|
||||
|
||||
5-whys:
|
||||
|
||||
1. **Por que a tela fica branca?** Porque a raiz do Payload não monta: `getFromImportMap` (node_modules/payload/dist/bin/generateImportMap/utilities/getFromImportMap.js) retorna undefined para o handler `S3ClientUploadHandler` que o config serializado do admin referencia.
|
||||
2. **Por que o handler não é encontrado?** Porque o importMap commitado não tem mais a entrada — o mapa é um artefato estático, não gerado em runtime.
|
||||
3. **Por que a entrada sumiu do arquivo?** Porque o commit 1c865bd8 regenerou o importMap com `pnpm generate:importmap` sem as envs `S3_*` no ambiente.
|
||||
4. **Por que sem as envs a entrada some?** Porque o plugin s3Storage é condicional em src/payload.config.ts (resolvedor src/utilities/mediaStorage.ts): sem `S3_*` o storage é local (dev/test/CI) e o mapa gerado é consistente para essa config; com `S3_*` setadas (produção, media no Garage) o config serializado do admin referencia o handler — que o mapa sem envs não inclui.
|
||||
5. **Por que ninguém viu antes do deploy?** Porque dev/test/CI não têm `S3_*` — o importMap órfão é invisível em todo o loop local (o e2e do admin passa), e o smoke test do deploy só checa HTTP 200 (tela branca passa). O erro real (`getFromImportMap: PayloadComponent not found in importMap`) vai para o log do SERVIDOR via console.error silencioso — o browser mostra zero erros. E o guard planejado nas lições do OPS69 (CI que regenera com envs dummy e falha se divergir, depends #77/#87) nunca foi implementado.
|
||||
|
||||
Classe recorrente: OPS69 (2026-08-19), OPS72 (2026-08-19), OPS73 (2026-08-20) — esta é a 4ª recorrência.
|
||||
|
||||
**Reprodução:** local, servindo o build com envs S3*\* dummy no runtime → /admin/login branco (hidden div de 17 bytes, form ausente) + log do servidor com `getFromImportMap: PayloadComponent not found in importMap`; sem S3*\* → login renderiza. A mesma assinatura de prod foi verificada por instrumentação (chunk pushes, r.m, flight data, headers, HTML brotli).
|
||||
|
||||
## Correção
|
||||
|
||||
Regeneração do importMap com envs S3\_\* dummy — o diff foi de exatamente +2 linhas (import + entrada do handler). Resolve a **causa** (o artefato volta a ser consistente com o config que produção serializa), não o sintoma. Sem migration, sem mudança de access, sem UI.
|
||||
|
||||
## Verificação
|
||||
|
||||
- Teste de regressão: `tests/unit/importMapS3UploadHandler.unit.spec.ts` — falha se a entrada sumir do arquivo; vermelho sem o fix, verde com
|
||||
- Guard: `scripts/check-importmap-s3.mjs` — regenera com S3\_\* dummy e sai 1 (com drift e restore do arquivo) se o commitado divergir; exit 1 sem o fix, exit 0 com
|
||||
- Suíte: unit completa 284 arquivos / 2682 testes verdes; `tsc --noEmit` e lint exit 0
|
||||
- E2E original: admin servido com S3\_\* dummy (PORT 4093) — login renderiza (título "Login - Payload", form Email/Senha), login admin@test.invalid funciona, dashboard "Painel de Controle - Payload" monta, log do servidor com 0 `getFromImportMap`, 0 erros de console
|
||||
- CI: gate:fast verde (284 arquivos, 2682 testes)
|
||||
- Prod: não verificado — aguarda deploy manual + confirmação do humano
|
||||
|
||||
## Prevenção
|
||||
|
||||
| Estratégia | Custo | Estado |
|
||||
| ------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------ |
|
||||
| Pin unit `importMapS3UploadHandler.unit.spec.ts` (falha se a entrada sumir) | barata | implementada agora (mesmo PR) |
|
||||
| Guard `scripts/check-importmap-s3.mjs` no ci-pr.yml (content guards) e no job verify do deploy.yml | barata | implementada agora (mesmo PR) |
|
||||
| Wrapper `scripts/generate-importmap.mjs` no `pnpm generate:importmap` (injeta S3\_\* dummy quando ausentes) | barata | implementada agora (mesmo PR) |
|
||||
| Contrato compartilhado `scripts/lib/importMapContract.mjs` (chave + prefixo do handler, chaves das envs S3) | barata | implementada agora (mesmo PR) |
|
||||
| AGENTS.md: instrução das envs S3\_\* no generate:importmap + referência aos guards | barata | implementada agora (mesmo PR) |
|
||||
| Gerar o importMap no BUILD (prebuild com S3\_\* via BuildKit secrets) em vez de commitá-lo | cara | documentada — não implementada neste fluxo |
|
||||
| Tornar o plugin s3Storage incondicional no config (adaptador local em dev) | cara | documentada — não implementada neste fluxo |
|
||||
| Smoke real de deploy: abrir /admin em headless e assertar o mount da raiz Payload (não só HTTP 200) | cara | documentada — não implementada neste fluxo |
|
||||
| Fail-fast/erro estruturado quando getFromImportMap retorna undefined (hoje é console.error silencioso no log do servidor) | cara | documentada — não implementada neste fluxo |
|
||||
|
||||
**Estratégia implementada:** pin unit que falha se o handler sumir do importMap; guard de CI+deploy que regenera com S3\_\* dummy e falha em divergência (drift + restore do arquivo); wrapper do `generate:importmap` que injeta as envs dummy — a regeneração local nunca mais orfa o handler; contrato compartilhado da chave/prefixo e das envs; doc no AGENTS.md.
|
||||
|
||||
**Estratégia documentada (cara):** gerar o importMap no build (elimina a classe: artefato gerado não drift; toca o pipeline do homeserver); tornar o s3Storage incondicional (estabiliza o importMap; muda storage local e caminho de media); smoke de deploy que abre /admin em headless e asserta o mount da raiz Payload; fail-fast estruturado quando getFromImportMap retorna undefined.
|
||||
|
||||
## Lições
|
||||
|
||||
- **Padrão "silencioso só em prod":** o bug não tem NENHUM sinal no loop local — dev/test/CI rodam sem S3\_\*, o importMap órfão é consistente para a config local, o e2e do admin passa, e o erro real sai como console.error no log do servidor enquanto o browser mostra zero erros. Quarta recorrência da mesma classe (OPS69/OPS72/OPS73). Diferenças de config entre prod e o loop local (envs condicionais) precisam de um espelho artificial (envs dummy) em pelo menos um ponto do pipeline.
|
||||
- **Lição registrada sem enforcement não é lição:** o guard de CI (regenerar com envs dummy e falhar em divergência) já estava escrito nas lições do OPS69 com depends #77/#87 — e nunca foi implementado. O custo de 3 recorrências seguidas é exatamente o custo de anotar e não executar. Lições pós-incidente deveriam virar Issue/guard no mesmo fluxo, não item de backlog.
|
||||
- **Smoke test que só checa HTTP 200 não é smoke de UI:** tela branca devolve 200 com SSR completo — o smoke do deploy validava transporte, não aplicação. Para apps que dependem de mount client-side (admin do Payload), o smoke precisa abrir a página em headless e assertar a montagem da raiz, ou o contrato precisa ser o log do servidor sem o erro conhecido.
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
"changelog:build": "node scripts/build-changelog.mjs",
|
||||
"changelog:read": "node scripts/build-changelog.mjs --stdout",
|
||||
"changelog:check": "node scripts/build-changelog.mjs --check",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload generate:importmap",
|
||||
"generate:importmap": "node scripts/generate-importmap.mjs",
|
||||
"generate:types": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload generate:types",
|
||||
"migrate": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate",
|
||||
"migrate:create": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate:create",
|
||||
|
|
|
|||
120
scripts/check-importmap-s3.mjs
Normal file
120
scripts/check-importmap-s3.mjs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Guard: the committed importMap must include the S3ClientUploadHandler entry
|
||||
* (OPS69/OPS72/OPS73 class — regenerating the importMap without the S3_* envs
|
||||
* silently orphans it, and the admin goes blank in production).
|
||||
*
|
||||
* Regenerates the importMap with dummy S3_* envs (same trick as the OPS69
|
||||
* fix — the CLI iterates the config without a DB) and fails if the committed
|
||||
* file drifts. The working-tree file is restored afterwards so a failure
|
||||
* leaves the checkout untouched; the drift is printed in the error.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { S3_STORAGE_ENV_KEYS } from './lib/importMapContract.mjs'
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const importMapPath = path.join(repoRoot, 'src/app/(payload)/admin/importMap.js')
|
||||
|
||||
if (!existsSync(importMapPath)) {
|
||||
console.error(`importMap not found at ${importMapPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const committed = readFileSync(importMapPath, 'utf8')
|
||||
|
||||
const GUARD_S3_DUMMY = {
|
||||
S3_BUCKET: 'guard-bucket',
|
||||
S3_ENDPOINT: 'http://127.0.0.1:3900',
|
||||
S3_ACCESS_KEY_ID: 'guard-access-key',
|
||||
S3_SECRET_ACCESS_KEY: 'guard-secret-key',
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
// Same NODE_OPTIONS as the package.json `generate:importmap` script —
|
||||
// the payload CLI loads server-only modules.
|
||||
NODE_OPTIONS: '--no-deprecation --conditions=react-server',
|
||||
// Dummy S3_* envs gate the s3Storage plugin in (see mediaStorage.ts) so the
|
||||
// generation is prod-faithful; the generator never connects to the bucket.
|
||||
...Object.fromEntries(S3_STORAGE_ENV_KEYS.map((key) => [key, GUARD_S3_DUMMY[key]])),
|
||||
// The payload config needs a secret and a database URL to build, even
|
||||
// though the generator never connects (iterateConfig, no DB).
|
||||
PAYLOAD_SECRET: process.env.PAYLOAD_SECRET || 'guard-secret',
|
||||
DATABASE_URL: process.env.DATABASE_URL || 'postgresql://guard:guard@127.0.0.1:1/guard',
|
||||
}
|
||||
|
||||
try {
|
||||
execFileSync('pnpm', ['exec', 'payload', 'generate:importmap'], {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
} catch (error) {
|
||||
restore(committed)
|
||||
console.error('guard-importmap: payload generate:importmap failed')
|
||||
console.error(error.stderr?.toString() || error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const generated = readFileSync(importMapPath, 'utf8')
|
||||
if (generated !== committed) {
|
||||
restore(committed)
|
||||
console.error(
|
||||
[
|
||||
'guard-importmap: the committed importMap drifted from the generation',
|
||||
'with S3_* envs set. Regenerate it with the S3_* envs (dummy values are',
|
||||
'fine) and commit the result:',
|
||||
'',
|
||||
' S3_BUCKET=x S3_ENDPOINT=http://127.0.0.1:3900 \\',
|
||||
' S3_ACCESS_KEY_ID=x S3_SECRET_ACCESS_KEY=x pnpm generate:importmap',
|
||||
'',
|
||||
'Drift (committed -> generated):',
|
||||
].join('\n'),
|
||||
)
|
||||
const drift = diffLines(committed, generated)
|
||||
console.error(drift)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Minimal line diff (no external deps): removed lines prefixed with "-",
|
||||
// added lines with "+".
|
||||
function diffLines(before, after) {
|
||||
const beforeLines = before.split('\n')
|
||||
const afterLines = after.split('\n')
|
||||
const out = []
|
||||
let i = 0
|
||||
let j = 0
|
||||
while (i < beforeLines.length && j < afterLines.length) {
|
||||
if (beforeLines[i] === afterLines[j]) {
|
||||
i += 1
|
||||
j += 1
|
||||
} else {
|
||||
const nextInBefore = beforeLines.indexOf(afterLines[j], i + 1)
|
||||
const nextInAfter = afterLines.indexOf(beforeLines[i], j + 1)
|
||||
if (nextInAfter === -1) {
|
||||
out.push(`- ${beforeLines[i]}`)
|
||||
i += 1
|
||||
} else if (nextInBefore === -1) {
|
||||
out.push(`+ ${afterLines[j]}`)
|
||||
j += 1
|
||||
} else if (nextInAfter - j <= nextInBefore - i) {
|
||||
out.push(`+ ${afterLines[j]}`)
|
||||
j += 1
|
||||
} else {
|
||||
out.push(`- ${beforeLines[i]}`)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
while (i < beforeLines.length) out.push(`- ${beforeLines[i++]}`)
|
||||
while (j < afterLines.length) out.push(`+ ${afterLines[j++]}`)
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
function restore(content) {
|
||||
writeFileSync(importMapPath, content)
|
||||
}
|
||||
35
scripts/generate-importmap.mjs
Normal file
35
scripts/generate-importmap.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Wrapper for `payload generate:importmap` that always injects dummy S3_*
|
||||
* envs (when unset) so the generated importMap carries the
|
||||
* S3ClientUploadHandler entry — regenerating without the S3_* envs silently
|
||||
* orphans it and the admin goes blank in production (class
|
||||
* OPS69/OPS72/OPS73). Dummy values are fine: the generator only iterates the
|
||||
* config, it never connects to the bucket.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process'
|
||||
|
||||
import { S3_STORAGE_ENV_KEYS } from './lib/importMapContract.mjs'
|
||||
|
||||
const DUMMY = {
|
||||
S3_BUCKET: 'dev-dummy-bucket',
|
||||
S3_ENDPOINT: 'http://127.0.0.1:3900',
|
||||
S3_ACCESS_KEY_ID: 'dev-dummy-access-key',
|
||||
S3_SECRET_ACCESS_KEY: 'dev-dummy-secret-key',
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
// Same NODE_OPTIONS as before — the payload CLI loads server-only modules.
|
||||
NODE_OPTIONS: '--no-deprecation --conditions=react-server',
|
||||
...Object.fromEntries(
|
||||
S3_STORAGE_ENV_KEYS.map((key) => [key, process.env[key]?.trim() || DUMMY[key]]),
|
||||
),
|
||||
}
|
||||
|
||||
const result = spawnSync('payload', ['generate:importmap'], {
|
||||
stdio: 'inherit',
|
||||
env,
|
||||
})
|
||||
|
||||
process.exit(result.status ?? 1)
|
||||
20
scripts/lib/importMapContract.mjs
Normal file
20
scripts/lib/importMapContract.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Contract shared by the importMap unit pin and the CI guard. The admin goes
|
||||
* blank in production when the importMap loses the S3 upload handler entry
|
||||
* (class OPS69/OPS72/OPS73 — the importMap regenerated without the S3_* envs).
|
||||
*/
|
||||
export const S3_UPLOAD_HANDLER_KEY = '@payloadcms/storage-s3/client#S3ClientUploadHandler'
|
||||
|
||||
export const S3_UPLOAD_HANDLER_IMPORT_PREFIX =
|
||||
'import { S3ClientUploadHandler as S3ClientUploadHandler_'
|
||||
|
||||
/**
|
||||
* Env keys that gate the s3Storage plugin (see src/utilities/mediaStorage.ts).
|
||||
* Kept in sync by the `importMapContract` unit pin.
|
||||
*/
|
||||
export const S3_STORAGE_ENV_KEYS = [
|
||||
'S3_BUCKET',
|
||||
'S3_ENDPOINT',
|
||||
'S3_ACCESS_KEY_ID',
|
||||
'S3_SECRET_ACCESS_KEY',
|
||||
]
|
||||
|
|
@ -36,6 +36,7 @@ import { ImportPreview as ImportPreview_cdf7e044479f899a31f804427d568b36 } from
|
|||
import { ImportSaveButton as ImportSaveButton_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { InstagramSyncStatusPanel as InstagramSyncStatusPanel_90dd38ffecf41d93e389c788f98f48da } from '../../../components/admin/InstagramSyncStatusPanel'
|
||||
import { InstagramPostExclusionPicker as InstagramPostExclusionPicker_ba03418db242c21470543bbeb06827a0 } from '../../../components/admin/InstagramPostExclusionPicker'
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||
import { ImportExportProvider as ImportExportProvider_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
|
|
@ -78,6 +79,7 @@ export const importMap = {
|
|||
"@payloadcms/plugin-import-export/rsc#ImportSaveButton": ImportSaveButton_cdf7e044479f899a31f804427d568b36,
|
||||
"./components/admin/InstagramSyncStatusPanel#InstagramSyncStatusPanel": InstagramSyncStatusPanel_90dd38ffecf41d93e389c788f98f48da,
|
||||
"./components/admin/InstagramPostExclusionPicker#InstagramPostExclusionPicker": InstagramPostExclusionPicker_ba03418db242c21470543bbeb06827a0,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||
"@payloadcms/plugin-import-export/rsc#ImportExportProvider": ImportExportProvider_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
}
|
||||
|
|
|
|||
29
tests/unit/importMapS3UploadHandler.unit.spec.ts
Normal file
29
tests/unit/importMapS3UploadHandler.unit.spec.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
S3_UPLOAD_HANDLER_IMPORT_PREFIX,
|
||||
S3_UPLOAD_HANDLER_KEY,
|
||||
} from '../../scripts/lib/importMapContract.mjs'
|
||||
|
||||
// The admin goes blank in production when this entry is missing: with the
|
||||
// S3_* envs set, the media collection uses the s3Storage plugin and the admin
|
||||
// config references this handler — getFromImportMap returns undefined, the
|
||||
// Payload root never mounts, and the admin renders an empty page with zero
|
||||
// console errors (class OPS69/OPS72/OPS73; orphaned again by 1c865bd8,
|
||||
// 2026-08-23). Regenerating the importMap without the S3_* envs silently
|
||||
// removes the entry — this pin fails the exact drift.
|
||||
|
||||
const importMapPath = path.resolve(__dirname, '../../src/app/(payload)/admin/importMap.js')
|
||||
|
||||
describe('importMap (admin)', () => {
|
||||
it('keeps the S3ClientUploadHandler entry required by the prod S3 storage', () => {
|
||||
const source = readFileSync(importMapPath, 'utf8')
|
||||
|
||||
// Import + map entry, like the generator emits (+2 lines in the file).
|
||||
expect(source).toContain(S3_UPLOAD_HANDLER_IMPORT_PREFIX)
|
||||
expect(source).toContain(`${S3_UPLOAD_HANDLER_KEY}": S3ClientUploadHandler_`)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue