Skip to content

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:

  1. Different build configurations between applications and libraries
  2. Inconsistent dependency resolution patterns
  3. Varying approaches to TypeScript compilation (tsc vs vite)
  4. Differing test and typecheck integration
  5. 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

  1. Consistency: Build configurations should follow the same pattern for all projects of the same type.
  2. Resilience: Builds should be resilient to common failures.
  3. Type Safety: TypeScript compilation should be enforced in the build process.
  4. Clear Dependencies: Build targets should explicitly declare their dependencies.
  5. Documentation: Build policies should be well-documented.

Standardized Build Targets

For TypeScript Libraries

Libraries will have a two-phase build process:

  1. build:vite: For bundled output that can be used in applications
  2. 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:

  1. build:retry: For handling flaky builds with automatic retry
  2. typecheck: A separate target for typechecking
  3. 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:

  1. Internal dependencies: Libraries should be built before applications
  2. Local dependencies: Package workspaces should be correctly focused
  3. Tool dependencies: All builds should depend on the install target

Implementation

The implementation will be rolled out in phases:

  1. Update library targets first (commons-ts, connections-ts, tools)
  2. Update application targets (datastream, orderbook, relayer, sequencer, websockets)
  3. 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

  1. Create template build configurations for libraries and applications
  2. Update each project with standardized configurations
  3. Add the new resilience targets to all projects
  4. Update documentation to reflect new build processes
  5. 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:

  1. Applications:

  2. datastream

  3. orderbook
  4. relayer
  5. sequencer
  6. websockets

  7. Libraries:

  8. commons-ts
  9. connections-ts
  10. tools

A verification script has been created to ensure all projects comply with the policy:

./scripts/check-project-configs.sh

Important Notes and Gotchas

During implementation, we discovered a few important issues to be aware of:

  1. 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.

  2. 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.

  3. Library dependencies for applications must be explicitly declared in the build target to ensure they are built in the correct order.

  4. When troubleshooting build issues, using the build:retry target with verbose logging can help identify the root cause of failures.