Skip to content
Closed
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
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Trivela development helpers
.PHONY: dev dev-down codegen codegen-check
.PHONY: dev dev-down dev-watch codegen codegen-check

# Issue #616: one-command local devnet (contracts + backend + frontend)
dev:
Expand All @@ -8,6 +8,10 @@ dev:
dev-down:
docker compose --profile devnet down -v

# Issue #1366: Docker Compose auto-reload file watcher
dev-watch:
docker compose watch

# Issue #615: regenerate TypeScript client from openapi.yaml
codegen:
cd sdk/client && npm install --silent && npm run generate
Expand Down
83 changes: 83 additions & 0 deletions backend/tests/composeWatch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import yaml from 'yaml';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '../../');

describe('Docker Compose Dev Watch Configuration (Issue #1366)', () => {
const composePath = path.join(rootDir, 'compose.yaml');

it('compose.yaml exists and is valid YAML', () => {
assert.ok(fs.existsSync(composePath), 'compose.yaml must exist');
const content = fs.readFileSync(composePath, 'utf8');
const doc = yaml.parse(content);
assert.ok(doc && typeof doc === 'object', 'compose.yaml must parse as an object');
assert.ok(doc.services, 'compose.yaml must have services');
});

it('backend service has develop.watch configured correctly', () => {
const content = fs.readFileSync(composePath, 'utf8');
const doc = yaml.parse(content);
const backend = doc.services.backend;
assert.ok(backend, 'backend service must exist in compose.yaml');
assert.ok(backend.develop, 'backend service must have develop block');
assert.ok(Array.isArray(backend.develop.watch), 'backend.develop.watch must be an array');

const srcSync = backend.develop.watch.find(
(w) => w.action === 'sync' && w.path === './backend/src'
);
assert.ok(srcSync, 'backend watch must include sync for ./backend/src');
assert.strictEqual(srcSync.target, '/app/backend/src');

const pkgRebuild = backend.develop.watch.find(
(w) => w.action === 'rebuild' && w.path === './backend/package.json'
);
assert.ok(pkgRebuild, 'backend watch must include rebuild for ./backend/package.json');
});

it('frontend service has develop.watch and CHOKIDAR_USEPOLLING configured', () => {
const content = fs.readFileSync(composePath, 'utf8');
const doc = yaml.parse(content);
const frontend = doc.services.frontend;
assert.ok(frontend, 'frontend service must exist in compose.yaml');
assert.strictEqual(
frontend.environment?.CHOKIDAR_USEPOLLING,
'true',
'frontend must define CHOKIDAR_USEPOLLING: "true"'
);
assert.ok(frontend.develop, 'frontend service must have develop block');
assert.ok(Array.isArray(frontend.develop.watch), 'frontend.develop.watch must be an array');

const srcSync = frontend.develop.watch.find(
(w) => w.action === 'sync' && w.path === './frontend/src'
);
assert.ok(srcSync, 'frontend watch must include sync for ./frontend/src');
assert.strictEqual(srcSync.target, '/app/frontend/src');

const publicSync = frontend.develop.watch.find(
(w) => w.action === 'sync' && w.path === './frontend/public'
);
assert.ok(publicSync, 'frontend watch must include sync for ./frontend/public');
assert.strictEqual(publicSync.target, '/app/frontend/public');

const pkgRebuild = frontend.develop.watch.find(
(w) => w.action === 'rebuild' && w.path === './frontend/package.json'
);
assert.ok(pkgRebuild, 'frontend watch must include rebuild for ./frontend/package.json');
});

it('frontend/vite.config.js references CHOKIDAR_USEPOLLING', () => {
const viteConfigPath = path.join(rootDir, 'frontend', 'vite.config.js');
assert.ok(fs.existsSync(viteConfigPath), 'vite.config.js must exist');
const viteContent = fs.readFileSync(viteConfigPath, 'utf8');
assert.ok(
viteContent.includes('CHOKIDAR_USEPOLLING'),
'vite.config.js must check process.env.CHOKIDAR_USEPOLLING'
);
});
});
18 changes: 18 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ services:
volumes:
- ./:/app
- backend_node_modules:/app/backend/node_modules
develop:
watch:
- action: sync
path: ./backend/src
target: /app/backend/src
- action: rebuild
path: ./backend/package.json

# Blue environment — serves production traffic by default.
# In a blue/green deployment this is the currently live backend.
Expand Down Expand Up @@ -68,13 +75,24 @@ services:
environment:
VITE_API_URL: http://backend:3001
VITE_STELLAR_NETWORK: testnet
CHOKIDAR_USEPOLLING: 'true'
depends_on:
- backend
ports:
- '5173:5173'
volumes:
- ./:/app
- frontend_node_modules:/app/frontend/node_modules
develop:
watch:
- action: sync
path: ./frontend/src
target: /app/frontend/src
- action: sync
path: ./frontend/public
target: /app/frontend/public
- action: rebuild
path: ./frontend/package.json

redis:
image: redis:7-alpine
Expand Down
15 changes: 15 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Development Guide

## Docker Compose File Watching & Auto-Reload

For containerized development with automated file synchronization and Hot Module Replacement (HMR),
Trivela configures native Docker Compose file watching. See the full
[Docker Compose Watch Guide](./DOCKER_COMPOSE_WATCH.md) for details.

Quick start:

```bash
# Run watcher on active compose stack
npm run dev:watch
# Or with make
make dev-watch
```

## Running Without Contracts

Both frontend and backend can run without deployed contracts. This allows development and testing
Expand Down
130 changes: 130 additions & 0 deletions docs/DOCKER_COMPOSE_WATCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Docker Compose Auto-Reload File Watcher Guide

## Overview

Trivela supports native Docker Compose File Watching via `develop.watch` (Docker Compose v2.22.0+).
This allows developers to edit code locally and have changes synchronize instantly with running
containers, triggering Hot Module Replacement (HMR) in the Vite frontend and instant code execution
in the Node.js backend without having to restart or rebuild containers manually.

---

## Architecture & Configuration

The root [`compose.yaml`](../compose.yaml) declares watch configurations for both the `backend` and
`frontend` services:

### 1. Backend Service (`backend`)

```yaml
develop:
watch:
- action: sync
path: ./backend/src
target: /app/backend/src
- action: rebuild
path: ./backend/package.json
```

- **`sync`**: Any changes to files inside `./backend/src` are synchronized immediately into
`/app/backend/src`. The backend service uses `nodemon` or Node file watchers to reload affected
modules.
- **`rebuild`**: Any modification to `./backend/package.json` (such as installing a new dependency)
automatically triggers an image rebuild and container restart.

### 2. Frontend Service (`frontend`)

```yaml
environment:
VITE_API_URL: http://backend:3001
VITE_STELLAR_NETWORK: testnet
CHOKIDAR_USEPOLLING: 'true'
develop:
watch:
- action: sync
path: ./frontend/src
target: /app/frontend/src
- action: sync
path: ./frontend/public
target: /app/frontend/public
- action: rebuild
path: ./frontend/package.json
```

- **`sync`**: Changes to `./frontend/src` and static assets in `./frontend/public` are mirrored into
the running container instantly. Vite's development server receives the event and sends Hot Module
Replacement (HMR) updates directly to connected browsers.
- **`rebuild`**: Changing `./frontend/package.json` prompts Docker Compose to rebuild the frontend
container image to guarantee consistent `node_modules`.
- **`CHOKIDAR_USEPOLLING="true"`**: Configured in `frontend/vite.config.js`
(`server.watch.usePolling`) to ensure file system notifications reliably cross hypervisor and
mount boundaries on Windows (Docker Desktop / WSL2) and macOS (VirtioFS / gRPC FUSE).

---

## Usage

### Prerequisites

- Docker Desktop or Docker Engine with Docker Compose v2.22.0 or newer.

### Starting Dev Environment with Watcher

You can run the watch process using any of the following convenient entry points:

1. **Using npm:**

```bash
npm run dev:watch
```

2. **Using make:**

```bash
make dev-watch
```

3. **Using Docker Compose CLI directly:**

```bash
# Run along with the default compose stack
docker compose watch

# Or launch and watch simultaneously
docker compose up --watch
```

---

## Cross-Platform File System Polling

When running Docker on Windows (WSL2 or Hyper-V) or macOS, standard Linux inotify events inside the
container may occasionally be missed when files are edited on the host OS.

Trivela handles this transparently:

1. `compose.yaml` sets `CHOKIDAR_USEPOLLING: "true"` on the frontend service.
2. `frontend/vite.config.js` inspects `process.env.CHOKIDAR_USEPOLLING`:
```javascript
server: {
port: 5173,
watch: {
usePolling: process.env.CHOKIDAR_USEPOLLING === 'true',
},
// ...
}
```
3. This guarantees that file change events are captured reliably across all host operating systems
without impacting Linux host performance where polling is disabled by default.

---

## Troubleshooting

- **Container does not pick up dependencies:** If you add a package to `package.json`, compose watch
triggers a rebuild. If your lockfile (`package-lock.json`) was also updated, run
`docker compose up --build` to force a clean installation.
- **Port conflicts:** Ensure ports `3001` (backend), `5173` (frontend), and `6379` (redis) are free
on your host machine before starting.
- **Logs:** To view logs alongside the watcher, run `docker compose logs -f backend frontend` in a
separate terminal.
3 changes: 3 additions & 0 deletions frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ export default defineConfig({
},
server: {
port: 5173,
watch: {
usePolling: process.env.CHOKIDAR_USEPOLLING === 'true',
},
proxy: {
'/api/v1': { target: 'http://localhost:3001', changeOrigin: true },
'/api': { target: 'http://localhost:3001', changeOrigin: true },
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"check:bundle": "node scripts/check-bundle-size.mjs",
"dev:devnet": "docker compose --profile devnet up --build",
"dev:devnet:down": "docker compose --profile devnet down -v",
"dev:watch": "docker compose watch",
"codegen": "cd sdk/client && npm install && npm run generate",
"codegen:check": "cd sdk/client && npm install && npm run check-drift"
},
Expand Down