Standardized Build Policy for NX Projects
Status | Date | Author |
---|---|---|
Accepted | 2025-03-21 | Claude |
Context
After reviewing the build targets across our NX monorepo, we've observed inconsistencies in how projects are built:
- Different build configurations between applications and libraries
- Inconsistent dependency resolution patterns
- Varying approaches to TypeScript compilation (tsc vs vite)
- Differing test and typecheck integration
- Unclear resilience strategies for builds
These inconsistencies lead to:
- Flaky builds in CI/CD pipelines
- Difficulty in understanding how to build specific projects
- Unexpected behavior when dependencies change
- Increased maintenance burden
Decision
We will standardize the build process across all TypeScript projects in the monorepo with a clear policy that addresses these inconsistencies:
Core Principles
- Consistency: Build configurations should follow the same pattern for all projects of the same type.
- Resilience: Builds should be resilient to common failures.
- Type Safety: TypeScript compilation should be enforced in the build process.
- Clear Dependencies: Build targets should explicitly declare their dependencies.
- Documentation: Build policies should be well-documented.
Standardized Build Targets
For TypeScript Libraries
Libraries will have a two-phase build process:
- build:vite: For bundled output that can be used in applications
- build: Uses tsc for generating declaration files and ensuring type safety
"build": {
"dependsOn": [
{
"dependencies": true,
"params": "ignore",
"target": "install"
},
{
"dependencies": true,
"params": "ignore",
"target": "build:vite"
}
],
"executor": "@nx/js:tsc",
"options": {
"assets": [
"{projectRoot}/*.md",
{
"glob": "package.json",
"input": "{projectRoot}",
"output": "."
}
],
"clean": true,
"generateExportsField": true,
"generatePackageJson": true,
"main": "{projectRoot}/src/index.ts",
"outputPath": "{projectRoot}/dist",
"rootDir": "{projectRoot}/src",
"tsConfig": "{projectRoot}/tsconfig.lib.json"
}
}
For TypeScript Applications
Applications will use Vite for bundling with proper resilience mechanisms and uniform build strategy:
"build": {
"executor": "nx:run-commands",
"dependsOn": [
{
"dependencies": true,
"params": "ignore",
"target": "install"
}
],
"options": {
"commands": [
"NODE_OPTIONS='--max-old-space-size=4096' nx run {project-name}:build:actual"
],
"cwd": "{projectRoot}"
}
},
"build:actual": {
"configurations": {
"development": {
"mode": "development",
"sourcemap": true
},
"production": {
"mode": "production",
"sourcemap": false
}
},
"defaultConfiguration": "production",
"executor": "@nx/vite:build",
"options": {
"bundle": true,
"configFile": "{projectRoot}/vite.config.ts",
"declaration": true,
"main": "{projectRoot}/src/index.ts",
"outputPath": "{projectRoot}/dist",
"skipTypeCheck": false,
"tsConfig": "{projectRoot}/tsconfig.app.json"
}
}
Additional Targets for Resilience
All TypeScript projects should include:
- build:retry: For handling flaky builds with automatic retry
- typecheck: A separate target for typechecking
- validate: A target that runs all validation steps
"build:retry": {
"executor": "nx:run-commands",
"options": {
"commands": [
"rm -rf ../../dist/{projectRoot}/* || true",
"NODE_OPTIONS='--max-old-space-size=4096' nx run {project-name}:build || (sleep 10 && NODE_OPTIONS='--max-old-space-size=4096' nx run {project-name}:build)"
],
"cwd": "{projectRoot}"
}
}
Dependency Resolution
All build targets must properly declare their dependencies:
- Internal dependencies: Libraries should be built before applications
- Local dependencies: Package workspaces should be correctly focused
- Tool dependencies: All builds should depend on the install target
Implementation
The implementation will be rolled out in phases:
- Update library targets first (commons-ts, connections-ts, tools)
- Update application targets (datastream, orderbook, relayer, sequencer, websockets)
- Add resilience mechanisms to all builds
Type Safety Policy
- Typechecking will be enforced during the build process
skipTypeCheck: false
should be standard except in development builds- Every project should have a separate
typecheck
target
Consequences
Positive
- More consistent and reliable builds
- Clearer dependency structure
- Better error detection during builds
- Improved CI/CD pipeline reliability
- Easier onboarding for new developers
Negative
- Initial effort to update all project configurations
- Slight increase in build time for some projects
- Additional complexity in build scripts
Compatibility
This policy is designed to work with both:
- TypeScript-only projects
- Projects with mixed TypeScript and JavaScript
Migration Plan
- Create template build configurations for libraries and applications
- Update each project with standardized configurations
- Add the new resilience targets to all projects
- Update documentation to reflect new build processes
- Run CI/CD pipeline to validate all builds work as expected
Implementation Status
All TypeScript applications and libraries have been updated to follow the standardized build policy:
-
Applications:
-
datastream
- orderbook
- relayer
- sequencer
-
websockets
-
Libraries:
- commons-ts
- connections-ts
- tools
A verification script has been created to ensure all projects comply with the policy:
Important Notes and Gotchas
During implementation, we discovered a few important issues to be aware of:
-
The
{workspaceRoot}
token in NX is only valid at the beginning of an option value. When used within a command string or in the middle of a value, it will cause errors. We've addressed this by using relative paths like../../dist/{projectRoot}
instead. -
TypeScript projects with mixed module types (ESM and CommonJS) might still experience some issues with imports when running the build. These require additional attention to the tsconfig settings.
-
Library dependencies for applications must be explicitly declared in the build target to ensure they are built in the correct order.
-
When troubleshooting build issues, using the
build:retry
target with verbose logging can help identify the root cause of failures.