Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 63 additions & 19 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,73 @@
// Dev container for T3 Code. Mirrors CI (ubuntu-24.04, Node 24, Rust stable)
// and the canonical setup in docs/internals/scripts.md: global `vp`, `vp i`.
// Contributor doc: docs/internals/devcontainer.md
{
"name": "T3 Code Dev",
"image": "debian:bookworm",
"name": "T3 Code",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers-extra/features/bun:1": {
"version": "1.3.11"
// nodeGypDependencies (default true) brings python3/make/g++, which Linux
// needs for node-pty's node-gyp fallback (its prebuilds are mac/win only).
"ghcr.io/devcontainers/features/node:2": {
"version": "24"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "24.13.1"
},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.10",
"installTools": false
}
// native/resource-monitor (edition 2024, needs stable >= 1.85). The server
// degrades gracefully without the binary, but CI checks cargo fmt + test.
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"hostRequirements": {
"cpus": 4,
"memory": "8gb",
"storage": "32gb"
},
"containerEnv": {
// Keep all runtime state inside the (gitignored) workspace .t3, matching
// the worktree default. An explicit --home-dir still wins.
"T3CODE_HOME": "${containerWorkspaceFolder}/.t3"
},
"overrideFeatureInstallOrder": [
"ghcr.io/devcontainers/features/git",
"ghcr.io/devcontainers-extra/features/bun"
"mounts": [
// vp keeps the pnpm content-addressable store and metadata cache under
// ~/.cache/pnpm (verified: the v11 store dir lives there); mounting a
// volume there lets installs survive container rebuilds.
"source=t3code-pnpm-store,target=/home/vscode/.cache/pnpm,type=volume",
// Root node_modules holds the whole pnpm virtual store (.pnpm), so one
// volume keeps the heavy tree off the slow macOS/Windows bind mount.
// Scoped by devcontainerId so parallel checkouts do not share it.
// Codespaces note: prebuild snapshots exclude volumes; drop these mounts
// if prebuilt codespaces become the primary workflow.
"source=t3code-node-modules-${devcontainerId},target=${containerWorkspaceFolder}/node_modules,type=volume"
],
"postCreateCommand": {
"bun-install": "bun install --backend=copyfile --frozen-lockfile"
"onCreateCommand": "bash .devcontainer/on-create.sh",
"updateContentCommand": "bash .devcontainer/update-content.sh",
"forwardPorts": [5733, 13773],
"portsAttributes": {
"5733": {
"label": "t3 web (open via the pairing URL, not the bare origin)",
"onAutoForward": "notify"
},
"13773": {
"label": "t3 server",
"onAutoForward": "silent"
}
},
"customizations": {
"codespaces": {
"openFiles": ["docs/internals/devcontainer.md"]
},
"vscode": {
"extensions": ["oxc.oxc-vscode"]
"extensions": ["oxc.oxc-vscode", "rust-lang.rust-analyzer"],
"settings": {
// .repos is a large vendored read-only reference tree; watching it
// burns CPU and file handles.
"files.watcherExclude": {
"**/.repos/**": true,
"**/.t3/**": true
},
"search.exclude": {
"**/.repos": true
}
}
}
}
},
"remoteUser": "vscode"
}
31 changes: 31 additions & 0 deletions .devcontainer/on-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# One-time container setup, baked into prebuilds. Content-dependent work
# (dependency install, Chromium) lives in update-content.sh.
set -euo pipefail

# The Vite+ CLI is the repo task runner (vp i, vp run dev, vp test run).
# Download to a file first: a curl failure inside $( ) would yield an empty
# script and a false success. VP_NODE_MANAGER=no skips the installer's node
# shims; Node comes from the devcontainer feature.
installer=$(mktemp)
curl -fsSL https://vite.plus -o "$installer"
VP_NODE_MANAGER=no bash "$installer"
rm -f "$installer"

# Non-login lifecycle shells never source the profile the installer edits,
# so expose vp on the default PATH. test -x keeps a layout change loud.
test -x "$HOME/.vite-plus/bin/vp"
sudo ln -sf "$HOME/.vite-plus/bin/vp" /usr/local/bin/vp

# First-run terminal notice, rendered by the devcontainers base image.
sudo mkdir -p /usr/local/etc/vscode-dev-containers
sudo tee /usr/local/etc/vscode-dev-containers/first-run-notice.txt >/dev/null <<'EOF'
T3 Code devcontainer

vp run dev start server + web, then open the pairing URL it
prints (the bare forwarded port will not authenticate)
cp .env.example .env optional: enable T3 Connect cloud features
(public identifiers, not secrets)

Details: docs/internals/devcontainer.md
EOF
19 changes: 19 additions & 0 deletions .devcontainer/update-content.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Runs at creation and on every prebuild content refresh, so codespaces start
# with deps installed and caches warm. Everything here is idempotent.
set -euo pipefail

# Volume mounts (pnpm store, node_modules) and the directories docker creates
# for them arrive root-owned; hand them to the dev user before installing.
for dir in "$HOME/.cache" "$HOME/.cache/pnpm" node_modules; do
if [ -d "$dir" ] && [ "$(stat -c %U "$dir")" != "$(id -un)" ]; then
sudo chown "$(id -un):$(id -gn)" "$dir"
fi
done

vp i
# Repairs electron's path.txt and exec bits after install, same as CI.
vp run --filter @t3tools/desktop ensure:electron
# Pre-warms Vite's dep optimizer (cache is keyed on the absolute path, which
# is stable inside the container).
node apps/web/scripts/warm-dep-cache.ts
32 changes: 32 additions & 0 deletions docs/internals/devcontainer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Dev container

> For maintainers. Using T3 Code? See [docs/user](../user/).

`.devcontainer/` gives you a ready-to-code Linux environment matching CI: Ubuntu 24.04, Node 24, pnpm, Rust stable, the global `vp` CLI, and the GitHub CLI. Open the repo in VS Code and "Reopen in Container", or create a GitHub Codespace. Dependency install (`vp i`), the Electron exec-bit repair, and the Vite dep-cache warmup all run automatically before you attach.

## What works in the container

- The full dev stack: `vp run dev`, then open the pairing URL it prints through the forwarded web port (5733). The bare origin is useless without the pairing token. In VS Code the forwarded port is a true localhost, so the printed URL works as-is; in browser Codespaces the forwarded origin differs, and if the server rejects it, pass the forwarded origin via `T3CODE_DEV_ALLOWED_ORIGINS`.
- Everything the Linux CI jobs run: `vp check`, `vp run typecheck`, `vp run test`, `vp run build:desktop`, and the resource-monitor cargo build and tests. (`vpr` is not on PATH here; the curl installer only shims `vp`. Use `vp run <script>` or `node_modules/.bin/vpr` after install.)

## State and safety

`T3CODE_HOME` points at the workspace's gitignored `.t3`, so all runtime state stays inside the container workspace, mirroring the worktree default. There is no live install to damage inside a container, but the test-data rule from AGENTS.md still holds: copy data in, never point at shared state.

## Caching

Two named volumes keep rebuilds fast and installs off the slow macOS/Windows bind mount: the pnpm store (shared across checkouts, mounted where `vp i` keeps it) and root `node_modules` (per-container, which covers the whole `.pnpm` virtual store since workspace packages just symlink into it). Deleting a container and recreating it reuses both, so a rebuild's `vp i` is seconds, not minutes. The host sees an empty `node_modules`; run host-side tooling inside the container.

## Out of scope

- Windowed Electron development is host-only. Building and verifying the desktop bundle works fine in the container (CI does exactly that, headless); launching the app needs a display.
- Mobile native builds are host-only (Xcode for iOS, Android SDK for Android). Typecheck, lint, and the mobile static checks run fine.
- `vp run dev --share` needs a tailscale binary and a tailnet; not provisioned here.

## Prebuilds

Container creation from scratch does a full `vp i` plus toolchain installs, which is worth prebuilding. Codespaces prebuilds are configured in repo settings, not files, and pick this config up as-is: the heavy steps live in `onCreateCommand` and `updateContentCommand`, which prebuilds bake in. Restrict prebuilds to one region and one retained version; storage bills per region per version. Note that prebuild snapshots exclude the caching volumes, so a prebuild-first workflow may prefer dropping the mounts. Outside Codespaces, the Dev Container CLI can push a prebuilt image:

```bash
devcontainer build --workspace-folder . --push true --image-name <registry>/t3code-devcontainer:latest
```
3 changes: 3 additions & 0 deletions docs/internals/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ vp run dev
Node 24 is required. Bun is not: the server picks Bun adapters when it detects Bun and falls back to
Node otherwise, and nothing in contributor setup needs it.

Prefer a container? `.devcontainer/` ships a ready-made CI-matching environment; see
[devcontainer.md](devcontainer.md).

`vp run dev` prints a one-time pairing URL. Open it so the first browser navigation is
authenticated.

Expand Down
Loading