## Summary Fixes `EMFILE: too many open files, watch` crash that most of the team is hitting on macOS when running `yarn start` or `npx nx start twenty-server`. Adds `rimraf dist` before `nest start --watch` in the `start` and `start:debug` targets, so the watcher starts with a clean output directory. ## Root cause The NestJS SWC compiler (`@nestjs/cli@11`) creates **three overlapping chokidar watchers** when `nest start --watch` runs: | Watcher | Watches | Purpose | Handles | |---|---|---|---| | SWC CLI (`@swc/cli`) | `src/` | Detects file changes → recompiles | ~1,730 | | NestJS `watchFilesInSrcDir` | `src/` | Workaround: SWC misses new files | shared with above | | NestJS `watchFilesInOutDir` | **`dist/`** | Detects compiled `.js` → restarts server | **~3,548** | `@nestjs/cli@11` ships with **chokidar v4**, which dropped macOS `fsevents` support and uses `fs.watch()` instead — creating **one file descriptor per directory**. Chokidar v3 used a single `fsevents` kernel subscription per directory tree. Total: **~5,000+ `fs.watch()` handles**, far exceeding the default macOS `ulimit -n` of ~2,560. ### Why it broke now PR #17851 (`15fc850212`) changed the `start` target from `dependsOn: ["build"]` to `dependsOn: ["^build"]`, removing the `rimraf dist && nest build` pre-step. Without that cleanup, `dist/` accumulated stale directories from code reorganizations (e.g. `application-layer/` → `application/` rename), growing to ~3,548 directories vs ~1,730 in a clean build. ## What this PR does Adds `rimraf dist &&` before `nest start --watch` in the `start` and `start:debug` commands. This ensures `dist/` starts empty and only contains directories matching the current `src/` structure (~1,730), keeping watcher count in the ~3,400 range. We still get the startup speed improvement from #17851 (no redundant full SWC build), since `rimraf dist` is ~instant while the removed `nest build` step took 30-60s. ## Future considerations As the codebase grows, even a clean `dist/` will eventually approach the macOS default `ulimit -n` (~2,560). Options to consider if that happens: 1. **Yarn resolution to force chokidar 3.6.0** — restores `fsevents`, reducing watcher count from ~5,000 to ~3-5. This is what Vite 7 does internally. Simple and effective, but pins to an older major version. 2. **Patch `@nestjs/cli`** to skip the `dist/` watcher — the `watchFilesInOutDir` watcher accounts for ~65% of all handles and only exists because NestJS doesn't have a direct hook into SWC's compilation-complete event. Could be removed via `yarn patch`. 3. **Replace `nest start --watch` entirely** — use `node --watch-path=src` (Node 22+) with `@swc-node/register` for on-the-fly compilation. Uses a single native watcher regardless of directory count. Requires rethinking asset copying (`watchAssets` in `nest-cli.json`). 4. **Wait for upstream fix** — NestJS CLI should either re-add `fsevents` support or use Node's recursive `fs.watch()` option (available since Node 20) instead of per-directory watchers. ## Test plan - [ ] Run `npx nx start twenty-server` on macOS — server starts without EMFILE error - [ ] Run `npx nx start:debug twenty-server` — debug mode starts without EMFILE error - [ ] Edit a `.ts` file while server is running — hot reload still works - [ ] Run `yarn start` (frontend + backend + worker) — no crashes Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com>
277 lines
8.2 KiB
JSON
277 lines
8.2 KiB
JSON
{
|
|
"name": "twenty-server",
|
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
"projectType": "application",
|
|
"tags": ["scope:backend"],
|
|
"targets": {
|
|
"build": {
|
|
"executor": "nx:run-commands",
|
|
"cache": true,
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"parallel": false,
|
|
"commands": ["rimraf dist", "nest build --path ./tsconfig.build.json"]
|
|
},
|
|
"dependsOn": ["^build"]
|
|
},
|
|
"test:integration": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"commands": [
|
|
"NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=12288 --import tsx/esm\" nx jest --config ./jest-integration.config.ts"
|
|
]
|
|
},
|
|
"parallel": false,
|
|
"configurations": {
|
|
"with-db-reset": {
|
|
"cwd": "packages/twenty-server",
|
|
"commands": [
|
|
"NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=12288 --import tsx/esm\" nx database:reset > reset-logs.log && NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=12288 --import tsx/esm\" nx jest --config ./jest-integration.config.ts"
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"build:packageJson": {
|
|
"executor": "@nx/js:tsc",
|
|
"options": {
|
|
"main": "packages/twenty-server/dist/main.js",
|
|
"tsConfig": "packages/twenty-server/tsconfig.json",
|
|
"outputPath": "packages/twenty-server/dist",
|
|
"updateBuildableProjectDepsInPackageJson": true
|
|
}
|
|
},
|
|
"typecheck": {
|
|
"dependsOn": ["^build"]
|
|
},
|
|
"start": {
|
|
"executor": "nx:run-commands",
|
|
"cache": false,
|
|
"dependsOn": ["^build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "rimraf dist && NODE_ENV=development nest start --watch"
|
|
}
|
|
},
|
|
"start:ci": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "NODE_ENV=development nest start"
|
|
}
|
|
},
|
|
"start:ci-if-needed": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "curl -f -s http://localhost:3000/healthz > /dev/null 2>&1 && echo '✅ Server already running' || (echo '🚀 Server not running, starting...' && nohup nest start &)"
|
|
}
|
|
},
|
|
"start:debug": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["^build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "rimraf dist && NODE_ENV=development nest start --watch --debug"
|
|
}
|
|
},
|
|
"reset:env": {
|
|
"executor": "nx:run-commands",
|
|
"inputs": ["{projectRoot}/.env.example"],
|
|
"outputs": ["{projectRoot}/.env"],
|
|
"cache": true,
|
|
"options": {
|
|
"cwd": "{projectRoot}",
|
|
"command": "cp .env.example .env"
|
|
}
|
|
},
|
|
"reset:env:e2e-testing-server": {
|
|
"executor": "nx:run-commands",
|
|
"inputs": ["{projectRoot}/.env.e2e-testing-server"],
|
|
"outputs": ["{projectRoot}/.env"],
|
|
"cache": true,
|
|
"options": {
|
|
"cwd": "{projectRoot}",
|
|
"command": "cp .env.e2e-testing-server .env"
|
|
}
|
|
},
|
|
"command": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "node dist/command/command.js"
|
|
}
|
|
},
|
|
"command-no-deps": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "node dist/command/command.js"
|
|
}
|
|
},
|
|
"worker": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "node dist/queue-worker/queue-worker.js"
|
|
}
|
|
},
|
|
"typeorm": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "ts-node --transpile-only -P tsconfig.json ../../node_modules/typeorm/cli.js"
|
|
},
|
|
"dependsOn": ["^build"]
|
|
},
|
|
"ts-node": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["^build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "ts-node"
|
|
}
|
|
},
|
|
"ts-node-no-deps": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "ts-node"
|
|
}
|
|
},
|
|
"ts-node-no-deps-transpile-only": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "ts-node --transpile-only"
|
|
}
|
|
},
|
|
"lint": {
|
|
"options": {
|
|
"lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"],
|
|
"maxWarnings": 0
|
|
},
|
|
"configurations": {
|
|
"ci": { "lintFilePatterns": ["{projectRoot}/**/*.{ts,json}"] },
|
|
"fix": {}
|
|
}
|
|
},
|
|
"lint:diff-with-main": {
|
|
"options": {
|
|
"pattern": "\\.(ts|tsx|js|jsx|json)$"
|
|
}
|
|
},
|
|
"test": {},
|
|
"test:debug": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register ../../node_modules/.bin/jest --runInBand"
|
|
}
|
|
},
|
|
"jest": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "jest"
|
|
}
|
|
},
|
|
"database:migrate": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["^build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"commands": [
|
|
"nx typeorm -- migration:run -d src/database/typeorm/core/core.datasource"
|
|
]
|
|
}
|
|
},
|
|
"database:migrate:generate": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "npx nx typeorm -- migration:generate src/database/typeorm/core/migrations/common/{args.migrationName} -d src/database/typeorm/core/core.datasource.ts"
|
|
}
|
|
},
|
|
"database:migrate:revert": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["^build"],
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"commands": [
|
|
"nx typeorm -- migration:revert -d src/database/typeorm/core/core.datasource"
|
|
]
|
|
}
|
|
},
|
|
"generate:integration-test": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"commands": [
|
|
"nx ts-node-no-deps -- ./test/integration/graphql/codegen/index.ts"
|
|
],
|
|
"parallel": false
|
|
}
|
|
},
|
|
"database:reset": {
|
|
"executor": "nx:run-commands",
|
|
"dependsOn": ["build"],
|
|
"configurations": {
|
|
"no-seed": {
|
|
"cwd": "packages/twenty-server",
|
|
"commands": [
|
|
"nx ts-node-no-deps-transpile-only -- ./scripts/truncate-db.ts",
|
|
"nx ts-node-no-deps-transpile-only -- ./scripts/setup-db.ts",
|
|
"nx database:migrate",
|
|
"nx command-no-deps -- cache:flush"
|
|
],
|
|
"parallel": false
|
|
},
|
|
"seed": {
|
|
"cwd": "packages/twenty-server",
|
|
"commands": [
|
|
"nx ts-node-no-deps-transpile-only -- ./scripts/truncate-db.ts",
|
|
"nx ts-node-no-deps-transpile-only -- ./scripts/setup-db.ts",
|
|
"nx database:migrate",
|
|
"nx command-no-deps -- cache:flush",
|
|
"nx command-no-deps -- workspace:seed:dev"
|
|
],
|
|
"parallel": false
|
|
}
|
|
},
|
|
"defaultConfiguration": "seed"
|
|
},
|
|
"clickhouse:migrate": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "nx ts-node-no-deps -- src/database/clickHouse/migrations/run-migrations.ts"
|
|
}
|
|
},
|
|
"clickhouse:seed": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "packages/twenty-server",
|
|
"command": "nx ts-node-no-deps -- src/database/clickHouse/seeds/run-seeds.ts"
|
|
}
|
|
},
|
|
"lingui:extract": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "{projectRoot}",
|
|
"command": "lingui extract --overwrite --clean"
|
|
}
|
|
},
|
|
"lingui:compile": {
|
|
"executor": "nx:run-commands",
|
|
"options": {
|
|
"cwd": "{projectRoot}",
|
|
"command": "lingui compile --typescript"
|
|
}
|
|
}
|
|
}
|
|
}
|