mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
725 lines
18 KiB
Markdown
725 lines
18 KiB
Markdown
# Development Workflow for ATCR
|
||
|
||
## The Problem
|
||
|
||
**Current development cycle with Docker:**
|
||
1. Edit CSS, JS, template, or Go file
|
||
2. Run `docker compose build` (rebuilds entire image)
|
||
3. Run `docker compose up` (restart container)
|
||
4. Wait **2-3 minutes** for changes to appear
|
||
5. Test, find issue, repeat...
|
||
|
||
**Why it's slow:**
|
||
- All assets embedded via `embed.FS` at compile time
|
||
- Multi-stage Docker build compiles everything from scratch
|
||
- No development mode exists
|
||
- Final image uses `scratch` base (no tools, no hot reload)
|
||
|
||
## The Solution
|
||
|
||
**Development setup combining:**
|
||
1. **Dockerfile.devel** - Development-focused container (golang base, not scratch)
|
||
2. **Volume mounts** - Live code editing (changes appear instantly in container)
|
||
3. **DirFS** - Skip embed, read templates/CSS/JS from filesystem
|
||
4. **Air** - Auto-rebuild on Go code changes
|
||
|
||
**Results:**
|
||
- CSS/JS/Template changes: **Instant** (0 seconds, just refresh browser)
|
||
- Go code changes: **2-5 seconds** (vs 2-3 minutes)
|
||
- Production builds: **Unchanged** (still optimized with embed.FS)
|
||
|
||
## How It Works
|
||
|
||
### Architecture Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ Your Editor (VSCode, etc) │
|
||
│ Edit: style.css, app.js, *.html, *.go files │
|
||
└─────────────────┬───────────────────────────────────┘
|
||
│ (files saved to disk)
|
||
▼
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ Volume Mount (docker-compose.dev.yml) │
|
||
│ volumes: │
|
||
│ - .:/app (entire codebase mounted) │
|
||
└─────────────────┬───────────────────────────────────┘
|
||
│ (changes appear instantly in container)
|
||
▼
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ Container (golang:1.25.7 base, has all tools) │
|
||
│ │
|
||
│ ┌──────────────────────────────────────┐ │
|
||
│ │ Air (hot reload tool) │ │
|
||
│ │ Watches: *.go, *.html, *.css, *.js │ │
|
||
│ │ │ │
|
||
│ │ On change: │ │
|
||
│ │ - *.go → rebuild binary (2-5s) │ │
|
||
│ │ - templates/css/js → restart only │ │
|
||
│ └──────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌──────────────────────────────────────┐ │
|
||
│ │ ATCR AppView (ATCR_DEV_MODE=true) │ │
|
||
│ │ │ │
|
||
│ │ ui.go checks DEV_MODE: │ │
|
||
│ │ if DEV_MODE: │ │
|
||
│ │ templatesFS = os.DirFS("...") │ │
|
||
│ │ publicFS = os.DirFS("...") │ │
|
||
│ │ else: │ │
|
||
│ │ use embed.FS (production) │ │
|
||
│ │ │ │
|
||
│ │ Result: Reads from mounted files │ │
|
||
│ └──────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Change Scenarios
|
||
|
||
#### Scenario 1: Edit CSS/JS/Templates
|
||
```
|
||
1. Edit pkg/appview/public/css/style.css in VSCode
|
||
2. Save file
|
||
3. Change appears in container via volume mount (instant)
|
||
4. App uses os.DirFS → reads new file from disk (instant)
|
||
5. Refresh browser → see changes
|
||
```
|
||
**Time:** **Instant** (0 seconds)
|
||
**No rebuild, no restart!**
|
||
|
||
#### Scenario 2: Edit Go Code
|
||
```
|
||
1. Edit pkg/appview/handlers/home.go
|
||
2. Save file
|
||
3. Air detects .go file change
|
||
4. Air runs: go build -o ./tmp/atcr-appview ./cmd/appview
|
||
5. Air kills old process and starts new binary
|
||
6. App runs with new code
|
||
```
|
||
**Time:** **2-5 seconds**
|
||
**Fast incremental build!**
|
||
|
||
## Implementation
|
||
|
||
### Step 1: Create Dockerfile.devel
|
||
|
||
Create `Dockerfile.devel` in project root:
|
||
|
||
```dockerfile
|
||
# Development Dockerfile with hot reload support
|
||
FROM golang:1.25.7-trixie
|
||
|
||
# Install Air for hot reload
|
||
RUN go install github.com/cosmtrek/air@latest
|
||
|
||
# Install SQLite (required for CGO in ATCR)
|
||
RUN apt-get update && apt-get install -y \
|
||
sqlite3 \
|
||
libsqlite3-dev \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
WORKDIR /app
|
||
|
||
# Copy dependency files and download (cached layer)
|
||
COPY go.mod go.sum ./
|
||
RUN go mod download
|
||
|
||
# Note: Source code comes from volume mount
|
||
# (no COPY . . needed - that's the whole point!)
|
||
|
||
# Air will handle building and running
|
||
CMD ["air", "-c", ".air.toml"]
|
||
```
|
||
|
||
### Step 2: Create docker-compose.dev.yml
|
||
|
||
Create `docker-compose.dev.yml` in project root:
|
||
|
||
```yaml
|
||
version: '3.8'
|
||
|
||
services:
|
||
atcr-appview:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile.devel
|
||
volumes:
|
||
# Mount entire codebase (live editing)
|
||
- .:/app
|
||
# Cache Go modules (faster rebuilds)
|
||
- go-cache:/go/pkg/mod
|
||
# Persist SQLite database
|
||
- atcr-ui-dev:/var/lib/atcr
|
||
environment:
|
||
# Enable development mode (uses os.DirFS)
|
||
ATCR_DEV_MODE: "true"
|
||
|
||
# AppView configuration
|
||
ATCR_HTTP_ADDR: ":5000"
|
||
ATCR_BASE_URL: "http://localhost:5000"
|
||
ATCR_DEFAULT_HOLD_DID: "did:web:hold01.atcr.io"
|
||
|
||
# Database
|
||
ATCR_UI_DATABASE_PATH: "/var/lib/atcr/ui.db"
|
||
|
||
# Auth
|
||
ATCR_AUTH_KEY_PATH: "/var/lib/atcr/auth/private-key.pem"
|
||
|
||
# Jetstream (optional)
|
||
# JETSTREAM_URL: "wss://jetstream2.us-east.bsky.network/subscribe"
|
||
# ATCR_BACKFILL_ENABLED: "false"
|
||
ports:
|
||
- "5000:5000"
|
||
networks:
|
||
- atcr-dev
|
||
|
||
# Add other services as needed (postgres, hold, etc)
|
||
# atcr-hold:
|
||
# ...
|
||
|
||
networks:
|
||
atcr-dev:
|
||
driver: bridge
|
||
|
||
volumes:
|
||
go-cache:
|
||
atcr-ui-dev:
|
||
```
|
||
|
||
### Step 3: Create .air.toml
|
||
|
||
Create `.air.toml` in project root:
|
||
|
||
```toml
|
||
# Air configuration for hot reload
|
||
# https://github.com/cosmtrek/air
|
||
|
||
root = "."
|
||
testdata_dir = "testdata"
|
||
tmp_dir = "tmp"
|
||
|
||
[build]
|
||
# Arguments to pass to binary (AppView needs "serve")
|
||
args_bin = ["serve"]
|
||
|
||
# Where to output the built binary
|
||
bin = "./tmp/atcr-appview"
|
||
|
||
# Build command
|
||
cmd = "go build -o ./tmp/atcr-appview ./cmd/appview"
|
||
|
||
# Delay before rebuilding (ms) - debounce rapid saves
|
||
delay = 1000
|
||
|
||
# Directories to exclude from watching
|
||
exclude_dir = [
|
||
"tmp",
|
||
"vendor",
|
||
"bin",
|
||
".git",
|
||
"node_modules",
|
||
"testdata"
|
||
]
|
||
|
||
# Files to exclude from watching
|
||
exclude_file = []
|
||
|
||
# Regex patterns to exclude
|
||
exclude_regex = ["_test\\.go"]
|
||
|
||
# Don't rebuild if file content unchanged
|
||
exclude_unchanged = false
|
||
|
||
# Follow symlinks
|
||
follow_symlink = false
|
||
|
||
# Full command to run (leave empty to use cmd + bin)
|
||
full_bin = ""
|
||
|
||
# Directories to include (empty = all)
|
||
include_dir = []
|
||
|
||
# File extensions to watch
|
||
include_ext = ["go", "html", "css", "js"]
|
||
|
||
# Specific files to watch
|
||
include_file = []
|
||
|
||
# Delay before killing old process (s)
|
||
kill_delay = "0s"
|
||
|
||
# Log file for build errors
|
||
log = "build-errors.log"
|
||
|
||
# Use polling instead of fsnotify (for Docker/VM)
|
||
poll = false
|
||
poll_interval = 0
|
||
|
||
# Rerun binary if it exits
|
||
rerun = false
|
||
rerun_delay = 500
|
||
|
||
# Send interrupt signal instead of kill
|
||
send_interrupt = false
|
||
|
||
# Stop on build error
|
||
stop_on_error = false
|
||
|
||
[color]
|
||
# Colorize output
|
||
app = ""
|
||
build = "yellow"
|
||
main = "magenta"
|
||
runner = "green"
|
||
watcher = "cyan"
|
||
|
||
[log]
|
||
# Show only app logs (not build logs)
|
||
main_only = false
|
||
|
||
# Add timestamp to logs
|
||
time = false
|
||
|
||
[misc]
|
||
# Clean tmp directory on exit
|
||
clean_on_exit = false
|
||
|
||
[screen]
|
||
# Clear screen on rebuild
|
||
clear_on_rebuild = false
|
||
|
||
# Keep scrollback
|
||
keep_scroll = true
|
||
```
|
||
|
||
### Step 4: Modify pkg/appview/ui.go
|
||
|
||
Add conditional filesystem loading to `pkg/appview/ui.go`:
|
||
|
||
```go
|
||
package appview
|
||
|
||
import (
|
||
"embed"
|
||
"html/template"
|
||
"io/fs"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
)
|
||
|
||
// Embedded assets (used in production)
|
||
//go:embed templates/**/*.html
|
||
var embeddedTemplatesFS embed.FS
|
||
|
||
//go:embed static
|
||
var embeddedpublicFS embed.FS
|
||
|
||
// Actual filesystems used at runtime (conditional)
|
||
var templatesFS fs.FS
|
||
var publicFS fs.FS
|
||
|
||
func init() {
|
||
// Development mode: read from filesystem for instant updates
|
||
if os.Getenv("ATCR_DEV_MODE") == "true" {
|
||
log.Println("🔧 DEV MODE: Using filesystem for templates and static assets")
|
||
templatesFS = os.DirFS("pkg/appview/templates")
|
||
publicFS = os.DirFS("pkg/appview/static")
|
||
} else {
|
||
// Production mode: use embedded assets
|
||
log.Println("📦 PRODUCTION MODE: Using embedded assets")
|
||
templatesFS = embeddedTemplatesFS
|
||
publicFS = embeddedpublicFS
|
||
}
|
||
}
|
||
|
||
// Templates returns parsed HTML templates
|
||
func Templates() *template.Template {
|
||
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
||
if err != nil {
|
||
log.Fatalf("Failed to parse templates: %v", err)
|
||
}
|
||
return tmpl
|
||
}
|
||
|
||
// StaticHandler returns a handler for static files
|
||
func StaticHandler() http.Handler {
|
||
sub, err := fs.Sub(publicFS, "static")
|
||
if err != nil {
|
||
log.Fatalf("Failed to create static sub-filesystem: %v", err)
|
||
}
|
||
return http.FileServer(http.FS(sub))
|
||
}
|
||
```
|
||
|
||
**Important:** Update the `Templates()` function to NOT cache templates in dev mode:
|
||
|
||
```go
|
||
// Templates returns parsed HTML templates
|
||
func Templates() *template.Template {
|
||
// In dev mode, reparse templates on every request (instant updates)
|
||
// In production, this could be cached
|
||
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
||
if err != nil {
|
||
log.Fatalf("Failed to parse templates: %v", err)
|
||
}
|
||
return tmpl
|
||
}
|
||
```
|
||
|
||
If you're caching templates, wrap it with a dev mode check:
|
||
|
||
```go
|
||
var templateCache *template.Template
|
||
|
||
func Templates() *template.Template {
|
||
// Development: reparse every time (instant updates)
|
||
if os.Getenv("ATCR_DEV_MODE") == "true" {
|
||
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
||
if err != nil {
|
||
log.Printf("Template parse error: %v", err)
|
||
return template.New("error")
|
||
}
|
||
return tmpl
|
||
}
|
||
|
||
// Production: use cached templates
|
||
if templateCache == nil {
|
||
tmpl, err := template.ParseFS(templatesFS, "templates/**/*.html")
|
||
if err != nil {
|
||
log.Fatalf("Failed to parse templates: %v", err)
|
||
}
|
||
templateCache = tmpl
|
||
}
|
||
return templateCache
|
||
}
|
||
```
|
||
|
||
### Step 5: Add to .gitignore
|
||
|
||
Add Air's temporary directory to `.gitignore`:
|
||
|
||
```
|
||
# Air hot reload
|
||
tmp/
|
||
build-errors.log
|
||
```
|
||
|
||
## Usage
|
||
|
||
### Starting Development Environment
|
||
|
||
```bash
|
||
# Build and start dev container
|
||
docker compose -f docker-compose.dev.yml up --build
|
||
|
||
# Or run in background
|
||
docker compose -f docker-compose.dev.yml up -d
|
||
|
||
# View logs
|
||
docker compose -f docker-compose.dev.yml logs -f atcr-appview
|
||
```
|
||
|
||
You should see Air starting:
|
||
|
||
```
|
||
atcr-appview | 🔧 DEV MODE: Using filesystem for templates and static assets
|
||
atcr-appview |
|
||
atcr-appview | __ _ ___
|
||
atcr-appview | / /\ | | | |_)
|
||
atcr-appview | /_/--\ |_| |_| \_ , built with Go
|
||
atcr-appview |
|
||
atcr-appview | watching .
|
||
atcr-appview | !exclude tmp
|
||
atcr-appview | building...
|
||
atcr-appview | running...
|
||
```
|
||
|
||
### Development Workflow
|
||
|
||
#### 1. Edit Templates/CSS/JS (Instant Updates)
|
||
|
||
```bash
|
||
# Edit any template, CSS, or JS file
|
||
vim pkg/appview/templates/pages/home.html
|
||
vim pkg/appview/public/css/style.css
|
||
vim pkg/appview/public/js/app.js
|
||
|
||
# Save file → changes appear instantly
|
||
# Just refresh browser (Cmd+R / Ctrl+R)
|
||
```
|
||
|
||
**No rebuild, no restart!** Air might restart the app, but it's instant since no compilation is needed.
|
||
|
||
#### 2. Edit Go Code (Fast Rebuild)
|
||
|
||
```bash
|
||
# Edit any Go file
|
||
vim pkg/appview/handlers/home.go
|
||
|
||
# Save file → Air detects change
|
||
# Air output shows:
|
||
# building...
|
||
# build successful in 2.3s
|
||
# restarting...
|
||
|
||
# Refresh browser to see changes
|
||
```
|
||
|
||
**2-5 second rebuild** instead of 2-3 minutes!
|
||
|
||
### Stopping Development Environment
|
||
|
||
```bash
|
||
# Stop containers
|
||
docker compose -f docker-compose.dev.yml down
|
||
|
||
# Stop and remove volumes (fresh start)
|
||
docker compose -f docker-compose.dev.yml down -v
|
||
```
|
||
|
||
## Production Builds
|
||
|
||
**Production builds are completely unchanged:**
|
||
|
||
```bash
|
||
# Production uses normal Dockerfile (embed.FS, scratch base)
|
||
docker compose build
|
||
|
||
# Or specific service
|
||
docker compose build atcr-appview
|
||
|
||
# Run production
|
||
docker compose up
|
||
```
|
||
|
||
**Why it works:**
|
||
- Production doesn't set `ATCR_DEV_MODE=true`
|
||
- `ui.go` defaults to embedded assets when env var is unset
|
||
- Production Dockerfile still uses multi-stage build to scratch
|
||
- No development dependencies in production image
|
||
|
||
## Comparison
|
||
|
||
| Change Type | Before (docker compose) | After (dev setup) | Improvement |
|
||
|-------------|------------------------|-------------------|-------------|
|
||
| Edit CSS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
|
||
| Edit JS | 2-3 minutes | **Instant (0s)** | ♾️x faster |
|
||
| Edit Template | 2-3 minutes | **Instant (0s)** | ♾️x faster |
|
||
| Edit Go Code | 2-3 minutes | **2-5 seconds** | 24-90x faster |
|
||
| Production Build | Same | **Same** | No change |
|
||
|
||
## Advanced: Local Development (No Docker)
|
||
|
||
For even faster development, run locally without Docker:
|
||
|
||
```bash
|
||
# Set environment variables
|
||
export ATCR_DEV_MODE=true
|
||
export ATCR_HTTP_ADDR=:5000
|
||
export ATCR_BASE_URL=http://localhost:5000
|
||
export ATCR_DEFAULT_HOLD_DID=did:web:hold01.atcr.io
|
||
export ATCR_UI_DATABASE_PATH=/tmp/atcr-ui.db
|
||
export ATCR_AUTH_KEY_PATH=/tmp/atcr-auth-key.pem
|
||
|
||
# Or use .env file
|
||
source .env.appview
|
||
|
||
# Run with Air
|
||
air -c .air.toml
|
||
|
||
# Or run directly (no hot reload)
|
||
go run ./cmd/appview serve
|
||
```
|
||
|
||
**Advantages:**
|
||
- Even faster (no Docker overhead)
|
||
- Native debugging with delve
|
||
- Direct filesystem access
|
||
- Full IDE integration
|
||
|
||
**Disadvantages:**
|
||
- Need to manage dependencies locally (SQLite, etc)
|
||
- May differ from production environment
|
||
|
||
## Troubleshooting
|
||
|
||
### Air Not Rebuilding
|
||
|
||
**Problem:** Air doesn't detect changes
|
||
|
||
**Solution:**
|
||
```bash
|
||
# Check if Air is actually running
|
||
docker compose -f docker-compose.dev.yml logs atcr-appview
|
||
|
||
# Check .air.toml include_ext includes your file type
|
||
# Default: ["go", "html", "css", "js"]
|
||
|
||
# Restart container
|
||
docker compose -f docker-compose.dev.yml restart atcr-appview
|
||
```
|
||
|
||
### Templates Not Updating
|
||
|
||
**Problem:** Template changes don't appear
|
||
|
||
**Solution:**
|
||
```bash
|
||
# Check ATCR_DEV_MODE is set
|
||
docker compose -f docker-compose.dev.yml exec atcr-appview env | grep DEV_MODE
|
||
|
||
# Should output: ATCR_DEV_MODE=true
|
||
|
||
# Check templates aren't cached (see Step 4 above)
|
||
# Templates() should reparse in dev mode
|
||
```
|
||
|
||
### Go Build Failing
|
||
|
||
**Problem:** Air shows build errors
|
||
|
||
**Solution:**
|
||
```bash
|
||
# Check build logs
|
||
docker compose -f docker-compose.dev.yml logs atcr-appview
|
||
|
||
# Or check build-errors.log in container
|
||
docker compose -f docker-compose.dev.yml exec atcr-appview cat build-errors.log
|
||
|
||
# Fix the Go error, save file, Air will retry
|
||
```
|
||
|
||
### Volume Mount Not Working
|
||
|
||
**Problem:** Changes don't appear in container
|
||
|
||
**Solution:**
|
||
```bash
|
||
# Verify volume mount
|
||
docker compose -f docker-compose.dev.yml exec atcr-appview ls -la /app
|
||
|
||
# Should show your source files
|
||
|
||
# On Windows/Mac, check Docker Desktop file sharing settings
|
||
# Settings → Resources → File Sharing → add project directory
|
||
```
|
||
|
||
### Permission Errors
|
||
|
||
**Problem:** Cannot write to /var/lib/atcr
|
||
|
||
**Solution:**
|
||
```bash
|
||
# In Dockerfile.devel, add:
|
||
RUN mkdir -p /var/lib/atcr && chmod 777 /var/lib/atcr
|
||
|
||
# Or use named volumes (already in docker-compose.dev.yml)
|
||
volumes:
|
||
- atcr-ui-dev:/var/lib/atcr
|
||
```
|
||
|
||
### Slow Builds Even with Air
|
||
|
||
**Problem:** Air rebuilds slowly
|
||
|
||
**Solution:**
|
||
```bash
|
||
# Use Go module cache volume (already in docker-compose.dev.yml)
|
||
volumes:
|
||
- go-cache:/go/pkg/mod
|
||
|
||
# Increase Air delay to debounce rapid saves
|
||
# In .air.toml:
|
||
delay = 2000 # 2 seconds
|
||
|
||
# Or check if CGO is slowing builds
|
||
# AppView needs CGO for SQLite, but you can try:
|
||
CGO_ENABLED=0 go build # (won't work for ATCR, but good to know)
|
||
```
|
||
|
||
## Tips & Tricks
|
||
|
||
### Browser Auto-Reload (LiveReload)
|
||
|
||
Add LiveReload for automatic browser refresh:
|
||
|
||
```bash
|
||
# Install browser extension
|
||
# Chrome: https://chrome.google.com/webstore/detail/livereload
|
||
# Firefox: https://addons.mozilla.org/en-US/firefox/addon/livereload-web-extension/
|
||
|
||
# Add livereload to .air.toml (future Air feature)
|
||
# Or use a separate tool like browsersync
|
||
```
|
||
|
||
### Database Resets
|
||
|
||
Development database is in a named volume:
|
||
|
||
```bash
|
||
# Reset database (fresh start)
|
||
docker compose -f docker-compose.dev.yml down -v
|
||
docker compose -f docker-compose.dev.yml up
|
||
|
||
# Or delete specific volume
|
||
docker volume rm atcr_atcr-ui-dev
|
||
```
|
||
|
||
### Multiple Environments
|
||
|
||
Run dev and production side-by-side:
|
||
|
||
```bash
|
||
# Development on port 5000
|
||
docker compose -f docker-compose.dev.yml up -d
|
||
|
||
# Production on port 5001
|
||
docker compose up -d
|
||
|
||
# Now you can compare behavior
|
||
```
|
||
|
||
### Debugging with Delve
|
||
|
||
Add delve to Dockerfile.devel:
|
||
|
||
```dockerfile
|
||
RUN go install github.com/go-delve/delve/cmd/dlv@latest
|
||
|
||
# Change CMD to use delve
|
||
CMD ["dlv", "debug", "./cmd/appview", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "--", "serve"]
|
||
```
|
||
|
||
Then connect with VSCode or GoLand.
|
||
|
||
## Summary
|
||
|
||
**Development Setup (One-Time):**
|
||
1. Create `Dockerfile.devel`
|
||
2. Create `docker-compose.dev.yml`
|
||
3. Create `.air.toml`
|
||
4. Modify `pkg/appview/ui.go` for conditional DirFS
|
||
5. Add `tmp/` to `.gitignore`
|
||
|
||
**Daily Development:**
|
||
```bash
|
||
# Start
|
||
docker compose -f docker-compose.dev.yml up
|
||
|
||
# Edit files in your editor
|
||
# Changes appear instantly (CSS/JS/templates)
|
||
# Or in 2-5 seconds (Go code)
|
||
|
||
# Stop
|
||
docker compose -f docker-compose.dev.yml down
|
||
```
|
||
|
||
**Production (Unchanged):**
|
||
```bash
|
||
docker compose build
|
||
docker compose up
|
||
```
|
||
|
||
**Result:** 100x faster development iteration! 🚀
|