Generate the Payload importMap at build time (Dockerfile) and dev time (predev hook) with dummy S3_* envs, eliminating the class of bugs where the admin goes blank because the S3ClientUploadHandler entry is missing (OPS69/OPS72/OPS73). Changes: - Dockerfile: add pnpm generate:importmap with dummy S3_* envs before next build - scripts/predev-importmap.mjs: predev hook that generates importMap if missing or stale - Delete scripts/check-importmap-s3.mjs (guard no longer needed) - Delete tests/unit/importMapS3UploadHandler.unit.spec.ts (unit pin no longer needed) - Remove check-importmap-s3.mjs step from CI workflows - Add importMap.js to .gitignore (build artifact, not committed) - Update AGENTS.md with new contract Closes #928
7.1 KiB
OPS99: Generate importMap during build with dummy S3_* envs
1. Reading of intention
Problem: The committed importMap.js loses the S3ClientUploadHandler entry when regenerated without S3** envs, causing the admin to go blank in production (class OPS69/OPS72/OPS73). The current guard mechanism (CI check + unit pin) catches drift but requires manual regeneration with dummy S3** envs.
Goal: Eliminate the committed importMap as the source of truth for the S3 upload handler. Instead, generate it during the Docker build with dummy S3_* envs, so the production image always has the correct entry.
Key insight: The importMap is a build artifact, not source code. It should be generated at build time with the same environment the production image will run in.
2. Recommended approach
Primary approach: Generate importMap in Dockerfile builder stage
Steps:
- Dockerfile builder stage: Add
pnpm generate:importmapwith dummy S3_* envs BEFOREnext build - Retire unit pin: Remove
tests/unit/importMapS3UploadHandler.unit.spec.ts - Simplify CI guard: Update
scripts/check-importmap-s3.mjsto verify the builder generates correctly (not that committed file matches) - Dev/worktree predev: Add a predev hook that generates importMap if missing or stale
- Update docs: Remove "rode generate:importmap" from AGENTS.md, add new contract
Options considered and rejected
| Option | Why rejected |
|---|---|
| Keep committed importMap + improve guard | perpetuates the drift problem; guard catches but doesn't prevent |
| Generate in CI only | dev/worktrees still need the file; adds CI dependency for local dev |
| Generate in next.config.js | Payload's generate:importmap is a CLI command, not a Next.js plugin |
| Store importMap in S3 | overengineering; the file is small and deterministic |
3. Verifiable phases
Phase 1: Dockerfile builder stage (production fix)
Files to modify:
Dockerfile: Add S3_* dummy envs and generate:importmap step
Changes:
# In builder stage, before next build:
RUN --mount=type=secret,id=database_url,env=DATABASE_URL \
--mount=type=secret,id=payload_secret,env=PAYLOAD_SECRET \
S3_BUCKET=build-dummy \
S3_ENDPOINT=http://127.0.0.1:3900 \
S3_ACCESS_KEY_ID=build-dummy \
S3_SECRET_ACCESS_KEY=build-dummy \
NEXT_OUTPUT_STANDALONE=1 \
NODE_OPTIONS="--no-deprecation --max-old-space-size=8000" \
pnpm exec payload generate:importmap && \
pnpm exec next build
Verification:
- Build Docker image locally:
docker build --target builder . - Check
src/app/(payload)/admin/importMap.jscontains S3ClientUploadHandler - Deploy to homeserver and verify admin loads
Phase 2: Retire unit pin and update CI guard
Files to modify:
- Delete:
tests/unit/importMapS3UploadHandler.unit.spec.ts - Modify:
scripts/check-importmap-s3.mjs
Changes to guard script:
- Remove the "regenerate and diff against committed" logic
- Instead, verify that the committed importMap is NOT required for production
- Or: remove the guard entirely since the builder now generates correctly
Verification:
- Run
pnpm test:unit(should pass without the deleted test) - Run CI checks (guard should pass or be removed)
Phase 3: Dev/worktree predev hook
Files to modify:
package.json: Addpredevscript- Create:
scripts/predev-importmap.mjs(or add to existingscripts/dev.mjs)
Changes:
{
"scripts": {
"predev": "node scripts/predev-importmap.mjs"
}
}
The predev script should:
- Check if
src/app/(payload)/admin/importMap.jsexists - If missing or stale (compare mtime with payload.config.ts), regenerate with dummy S3_* envs
- Use the same dummy values as the CI guard
Verification:
- Delete importMap.js, run
pnpm dev→ file should be regenerated - Modify payload.config.ts, run
pnpm dev→ file should be regenerated
Phase 4: Documentation updates
Files to modify:
AGENTS.md: Update "rode generate:importmap" ruledocs/AGENT-OPS.md: Update if present
New contract:
- ImportMap is generated at build time (Docker) and dev time (predev hook)
- Never commit importMap.js to repo (add to .gitignore)
- CI guard can be removed or simplified to verify builder works
4. Rabbit holes / out of scope
Rabbit holes (avoid)
- Trying to make Payload auto-generate importMap - This would require modifying Payload core, out of scope
- Moving S3 config to runtime - The gating is intentional; keep it
- Changing the S3 storage plugin - Only the importMap generation changes
- Modifying the deploy script - The builder stage handles it now
Out of scope
- Changing how Payload generates the importMap (internal to Payload)
- Modifying the S3 storage plugin behavior
- Changing the media collection configuration
- Updating the CI pipeline beyond the guard removal
5. Risks and mitigation
| Risk | Impact | Mitigation |
|---|---|---|
| Builder fails to generate importMap | Admin breaks in production | Keep committed importMap as fallback in .gitignore (not tracked) |
| Dev predev hook slows startup | Developer experience | Only regenerate if file missing or stale (check mtime) |
| Dummy S3_* envs cause side effects | Unexpected behavior during build | Use clearly dummy values; Payload's generator doesn't connect to S3 |
| ImportMap generation order matters | Build fails | Generate BEFORE next build (as planned) |
6. Engineering acceptance
Acceptance criteria
- Production image has correct importMap with S3ClientUploadHandler
- Admin loads in production without manual importMap regeneration
- Dev workflow auto-generates importMap when needed
- CI checks pass (guard either updated or removed)
- No committed importMap in git history (add to .gitignore)
Verification steps
docker build --target builder .succeeds and generates importMappnpm devgenerates importMap if missingpnpm test:unitpasses (without deleted test)- Deploy to homeserver: admin loads correctly
git statusshows importMap.js as untracked (in .gitignore)
Decision quality self-score: 4/5
- Caro vs barato: Deliberate about what's expensive to reverse (committed importMap vs generated)
- Form: Options considered, recommendation clear
- Appetite: Respects the intention's scope (build-time generation)
- Depth check: Reuses existing
generate:importmapscript, no shallow pass-throughs - Rabbit holes: Named explicitly (Payload core, S3 plugin changes)