NOCTA UI

Monorepo Setup

Link a shared Nocta UI workspace to an application using Bun workspaces.

Nocta UI treats shared UI workspaces as the source of truth for component files. In a monorepo you can run CLI commands from an application workspace while the actual React source lives inside a sibling package. This guide recreates the Bun + Next.js example step by step so you can adapt it to your own tooling.

Prerequisites

  • Bun 1.1 or newer
  • Node.js (for occasional npx utilities)
  • Git (recommended for tracking generated files)

Repository Layout

nocta-bun-monorepo
├── apps/
│   └── web     (Next.js application)
└── packages/
    └── ui      (shared Nocta UI workspace)

Step-by-step

1. Initialise the repo root

 mkdir nocta-bun-monorepo
 cd nocta-bun-monorepo
 bun init --yes

Update package.json so Bun knows where workspaces live:

{
  "name": "nocta-bun-monorepo",
  "module": "index.ts",
  "type": "module",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],  
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "typescript": "^5"
  }
}

2. Scaffold the shared UI workspace (packages/ui)

mkdir -p packages/ui
cd packages/ui
bun init --yes
bun add -D tsc-alias rimraf concurrently tailwindcss

Configure TypeScript with alias support so registry files that import from @/... compile cleanly:

{
  "compilerOptions": {
    "lib": ["ESNext","DOM"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": false,   
    "verbatimModuleSyntax": false,  
    "outDir": "dist",   
    "declaration": true,   
    "declarationDir": "dist",   
    "baseUrl": ".",   
    "paths": {   
      "@/*": ["./src/*"]   
    },   
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noPropertyAccessFromIndexSignature": false
  }
}

Add build scripts so the package emits compiled artefacts to dist/:

{
  "name": "ui",
  "type": "module",
  "private": true,
  "files": ["dist"],   
  "exports": {   
    ".": {   
      "import": "./dist/src/index.js",   
      "types": "./dist/src/index.d.ts"
    },   
    "./dist/styles.css": "./dist/styles.css"
  },   
  "main": "./dist/src/index.js",   
  "module": "./dist/src/index.js",   
  "types": "./dist/src/index.d.ts",   
  "peerDependencies": {
    "typescript": "^5.9.3"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "concurrently": "^9.2.1",
    "rimraf": "^6.0.1",
    "tailwindcss": "^4.1.15",
    "tsc-alias": "^1.8.16",
    "typescript": "^5.9.3"
  },
  "scripts": {   
    "build": "bun run clean && bun run build:css && bun run build:ts",   
    "build:css": "mkdir -p dist && bunx tailwindcss --input src/styles.css --output dist/styles.css --minify",   
    "build:ts": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",   
    "clean": "rimraf dist",   
    "dev": "concurrently -k -n TS,ALIAS,CSS \"bun run dev:ts\" \"bun run dev:alias\" \"bun run dev:css\"",   
    "dev:ts": "tsc -w -p tsconfig.json",   
    "dev:alias": "tsc-alias -w -p tsconfig.json",   
    "dev:css": "bunx tailwindcss --input src/styles.css --output dist/styles.css --watch"
  }   
}

tsc-alias rewrites the @/... imports in the compiled output so downstream apps can resolve files without custom TS paths.

3. Set up the Next.js app (apps/web)

 cd ../../
 mkdir -p apps/web
 cd apps/web
 bun create next-app@latest . --yes

Point the app at the shared workspace via Bun workspaces:

{
  "name": "web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "next": "16.0.0",
    "react": "19.2.0",
    "react-dom": "19.2.0",
    "ui": "workspace:*"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

Import the compiled UI stylesheet once inside app/layout.tsx (or your preferred root file):

import "ui/dist/styles.css";

4. Initialise the Nocta CLI

Run init inside each workspace so the CLI records relationships and writes helpers.

cd packages/ui
npx @nocta-ui/cli init                # choose Shared UI

cd ../../apps/web
npx @nocta-ui/cli init                # choose Application and link "ui"
  • The shared UI workspace receives nocta.config.json, helper utilities, Tailwind tokens, and an entry inside nocta.workspace.json.
  • The application workspace stores a link to ui, so future nocta-ui add runs from apps/web copy React source into packages/ui.

5. Install components and rebuild the shared package

From apps/web install components like normal:

npx @nocta-ui/cli add button card

The CLI writes component source files into packages/ui/src/..., updates export barrels, and scopes dependency installs to the owning workspace. Rebuild or watch the shared package after each run so the compiled artefacts remain fresh:

bun run --filter ui build
# or keep everything hot
bun run --filter ui dev

Using the Components

After rebuilding, import components from the shared package inside your application:

import { Button } from "ui";

export default function Page() {
  return <Button>Ready for launch</Button>;
}

You now have a monorepo-aware workflow where shared UI code lives in packages/ui, applications call CLI commands from their own directory, and Bun handles dependencies for the entire workspace.