Dependency Management
How τjs resolves and optimises dependencies across multiple applications.
Overview
Section titled “Overview”τjs uses a single dependency tree at the project root. Vite’s tree-shaking removes unused code from each bundle at build time. This means:
- One
node_modulesdirectory at project root - Each app declares its dependencies in its own
package.json - Dependencies are hoisted to root by package manager
- Each app bundle only includes what it imports
Key insight: The sise of node_modules doesn’t affect bundle sise. Only imports determine what’s included in each bundle.
How It Works
Section titled “How It Works”Dependency Resolution
Section titled “Dependency Resolution”project/├── package.json # Root package.json with workspaces├── node_modules/ # Single dependency tree (hoisted)│ ├── react/│ ├── react-dom/│ ├── @tanstack/react-query/│ ├── zustand/│ └── ...│└── client/ ├── app/ │ └── package.json # Declares: react, react-query └── admin/ └── package.json # Declares: react, zustandBuild-Time Tree-Shaking
Section titled “Build-Time Tree-Shaking”When τjs builds each app:
- Vite analses imports in app code
- Only includes imported modules in bundle
- Shared dependencies optimised automatically
- Unused code removed from final bundle
Example:
Customer App Build:─────────────────────────────────────────Scans imports in client/app/- import React from 'react' ✓ Included- import { useQuery } from '@tanstack/react-query' ✓ Included- (zustand never imported) ✗ Excluded
Bundle: React + react-querySize: 1.6MB
Admin App Build:─────────────────────────────────────────Scans imports in client/admin/- import React from 'react' ✓ Included- import { create } from 'zustand' ✓ Included- (@tanstack/react-query never imported) ✗ Excluded
Bundle: React + zustandSize: 1.3MBResult: Each app only ships code it actually uses.
Workspace Setup
Section titled “Workspace Setup”Using npm Workspaces
Section titled “Using npm Workspaces”// package.json (root){ "name": "my-project", "workspaces": ["client/*"], "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }}{ "name": "@my-project/app", "dependencies": { "@tanstack/react-query": "^5.0.0" }}{ "name": "@my-project/admin", "dependencies": { "zustand": "^4.0.0" }}Installation:
# Install all dependenciesnpm install
# Install app-specific dependencycd client/appnpm install @tanstack/react-query
# Install admin-specific dependencycd client/adminnpm install zustandHow it works:
- npm hoists dependencies to root
node_modules - Each app’s
package.jsondeclares what it needs - Workspace manager links packages together
- Build time: Vite resolves from root
node_modules
Workspace Managers
Section titled “Workspace Managers”τjs works with any workspace manager:
npm workspaces (built-in to npm 7+):
{ "workspaces": ["client/*"]}pnpm workspaces:
packages: - "client/*"Yarn workspaces:
{ "workspaces": ["client/*"], "packageManager": "yarn@4.0.0"}All achieve the same goal: Single dependency tree, per-app declarations.
Practical Workflow
Section titled “Practical Workflow”Root Dependencies (Shared)
Section titled “Root Dependencies (Shared)”Install dependencies used by all apps at root:
# At project rootnpm install react react-dom{ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }}App-Specific Dependencies
Section titled “App-Specific Dependencies”Install dependencies used by specific apps:
# Customer app needs react-querycd client/appnpm install @tanstack/react-query
# Admin app needs zustandcd client/adminnpm install zustand
# Both are hoisted to root node_modulesResult:
node_modules/├── react/ # Used by both├── react-dom/ # Used by both├── @tanstack/react-query/ # Only bundled in customer app└── zustand/ # Only bundled in admin appShared Code Between Apps
Section titled “Shared Code Between Apps”Shared Directory
Section titled “Shared Directory”Create a shared directory for code used across apps:
client/├── shared/│ ├── components/│ │ ├── Button.tsx│ │ └── Card.tsx│ ├── hooks/│ │ └── useAuth.ts│ └── utils/│ └── formatting.ts├── app/└── admin/Path Aliases
Section titled “Path Aliases”Configure aliases to import shared code:
// tsconfig.json{ "compilerOptions": { "paths": { "@shared/*": ["./client/shared/*"], "@app/*": ["./client/app/*"], "@admin/*": ["./client/admin/*"] } }}τjs automatically resolves these during build.
Using Shared Code
Section titled “Using Shared Code”import { Button } from "@shared/components/Button";import { useAuth } from "@shared/hooks/useAuth";import { formatDate } from "@shared/utils/formatting";
export function Dashboard() { const { user } = useAuth();
return ( <div> <h1>Welcome, {user.name}</h1> <Button>Click me</Button> <p>Joined: {formatDate(user.createdAt)}</p> </div> );}import { Button } from "@shared/components/Button"; // Same componentimport { formatDate } from "@shared/utils/formatting";
export function UserList({ users }) { return ( <div> {users.map((user) => ( <div key={user.id}> <span>{formatDate(user.createdAt)}</span> <Button>Edit</Button> </div> ))} </div> );}Build behavior:
Button.tsxcompiled once by Vite- Included in both app bundles
- Optimised automatically
- No duplication of compiled code
Dependency Version Management
Section titled “Dependency Version Management”Shared Major Versions
Section titled “Shared Major Versions”Apps should coordinate on major versions of shared dependencies:
// Root package.json{ "dependencies": { "react": "^19.0.0", // All apps use React 19 "react-dom": "^19.0.0" }}Why: Different React versions can cause runtime issues.
Independent Library Versions
Section titled “Independent Library Versions”Apps can use different versions of independent libraries:
{ "dependencies": { "@tanstack/react-query": "^5.0.0" }}{ "dependencies": { "@tanstack/react-query": "^4.0.0" // Different version OK }}Package managers handle version resolution - each app gets the version it needs.
Common Scenarios
Section titled “Common Scenarios”Adding a New Dependency
Section titled “Adding a New Dependency”To all apps:
# At rootnpm install lodash
# All apps can now import lodashTo specific app:
# In app directorycd client/appnpm install date-fns
# Only customer app can import date-fns# Only customer bundle includes itUpgrading a Shared Dependency
Section titled “Upgrading a Shared Dependency”# At rootnpm install react@latest react-dom@latest
# All apps now use updated React# Rebuild to pick up changesnpm run buildRemoving a Dependency
Section titled “Removing a Dependency”# Remove from app package.jsoncd client/appnpm uninstall @tanstack/react-query
# Clean installcd ../..npm install
# Rebuildnpm run buildBest Practices
Section titled “Best Practices”1. Declare Dependencies Explicitly
Section titled “1. Declare Dependencies Explicitly”// ✅ Good - app declares what it needs{ "dependencies": { "@tanstack/react-query": "^5.0.0", "date-fns": "^2.0.0" }}
// ❌ Bad - relying on hoisted dependency without declaration// (might break if other app removes it)2. Keep Shared Dependencies at Root
Section titled “2. Keep Shared Dependencies at Root”// ✅ Good - shared by all apps// Root package.json{ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }}3. Regular Dependency Audits
Section titled “3. Regular Dependency Audits”# Check for outdated dependenciesnpm outdated
# Check for security issuesnpm audit
# Update dependenciesnpm update4. Lock File Discipline
Section titled “4. Lock File Discipline”# Commit lock filegit add package-lock.json
# Don't commit node_modulesecho "node_modules" >> .gitignore