This manual shows how to create a fresh monorepo that uses a Git submodule for UI libraries, run a working Vite app (Hello World) that imports @ui8kit/core/Button, and maintain or remove the submodule cleanly.
Important: Cloning a repository that contains submodules will produce an empty directory for the submodule until you run:
git submodule update --init --recursive- Git 2.40+
- Bun 1.1.30 (recommended)
- macOS/Linux/Windows with terminal access
your-monorepo/
apps/
dash/ # Vite + React (SWC) app
packages/
@ui8kit/ # Git submodule: https://github.com/buildy-ui/ui8kit
core/
theme/
hooks/
form/
flow/
chat/
brain/
mkdir your-monorepo && cd your-monorepo
git init
echo node_modules> .gitignore
echo dist>> .gitignore
echo build>> .gitignore
mkdir -p apps/dash packagesCreate package.json in the repository root (aligns with provided example):
{
"name": "crm",
"version": "0.0.1",
"private": true,
"license": "GPL-3.0",
"type": "module",
"author": {
"name": "buildy",
"url": "https://buildy.tw"
},
"workspaces": [
"apps/*",
"packages/*",
"packages/@ui8kit/*"
],
"scripts": {
"dev": "bunx turbo run dev",
"build": "bunx turbo run build",
"test": "bunx turbo run test",
"lint": "bunx turbo run lint",
"dev:dash": "bunx turbo run dev --filter=./apps/dash",
"build:dash": "bunx turbo run build --filter=./apps/dash"
},
"packageManager": "bun@1.1.30",
"devDependencies": {
"turbo": "^2.5.6"
}
}Create turbo.json in the repository root:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}Create base tsconfig.json in the repository root (paths align with submodule contents):
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Base TypeScript Config",
"compilerOptions": {
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"moduleResolution": "bundler",
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"preserveWatchOutput": true,
"composite": true,
"incremental": true,
"baseUrl": ".",
"paths": {
"@ui8kit/core": ["./packages/@ui8kit/core/src"],
"@ui8kit/core/*": ["./packages/@ui8kit/core/src/*"],
"@ui8kit/theme": ["./packages/@ui8kit/theme/src"],
"@ui8kit/theme/*": ["./packages/@ui8kit/theme/src/*"],
"@ui8kit/hooks": ["./packages/@ui8kit/hooks/src"],
"@ui8kit/hooks/*": ["./packages/@ui8kit/hooks/src/*"],
"@ui8kit/form": ["./packages/@ui8kit/form/src"],
"@ui8kit/form/*": ["./packages/@ui8kit/form/src/*"],
"@ui8kit/flow": ["./packages/@ui8kit/flow/src"],
"@ui8kit/flow/*": ["./packages/@ui8kit/flow/src/*"],
"@ui8kit/chat": ["./packages/@ui8kit/chat/src"],
"@ui8kit/chat/*": ["./packages/@ui8kit/chat/src/*"],
"@ui8kit/brain": ["./packages/@ui8kit/brain/src"],
"@ui8kit/brain/*": ["./packages/@ui8kit/brain/src/*"]
}
},
"exclude": ["node_modules", "dist", "build"]
}Place the UI kit as a Git submodule at packages/@ui8kit:
git submodule add https://github.com/buildy-ui/ui8kit.git packages/@ui8kit
git submodule update --init --recursiveNotes:
- If you cloned without
--recurse-submodules,packages/@ui8kitwill be empty until you run the update command above. - To clone and populate submodules in one step, use:
git clone --recurse-submodules <repo-url>.
Create apps/dash/package.json (aligned with the provided example):
{
"name": "@buildy/dash",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"dev:crud": "cross-env VITE_ITEM_SCHEMA=crud vite",
"dev:qdrant": "cross-env VITE_ITEM_SCHEMA=qdrant vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@ui8kit/core": "workspace:*",
"@ui8kit/form": "workspace:*",
"@ui8kit/hooks": "workspace:*",
"@ui8kit/theme": "workspace:*",
"@ui8kit/flow": "workspace:*",
"@ui8kit/chat": "workspace:*",
"@dqbd/tiktoken": "^1.0.22",
"@xyflow/react": "^12.8.4",
"lucide-react": "^0.525.0",
"openai": "^5.19.1",
"react-day-picker": "^9.8.1",
"react-resizable-panels": "^3.0.4",
"react-router-dom": "^7.9.1",
"zod": "^4.1.5"
},
"devDependencies": {
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.18",
"class-variance-authority": "^0.7.0",
"cross-env": "^10.0.0",
"postcss": "^8.4.35",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^2.2.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.5.3",
"vite": "^5.4.1"
}
}Create apps/dash/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@ui8kit/core": ["../../packages/@ui8kit/core/src"],
"@ui8kit/core/*": ["../../packages/@ui8kit/core/src/*"],
"@ui8kit/theme": ["../../packages/@ui8kit/theme/src"],
"@ui8kit/theme/*": ["../../packages/@ui8kit/theme/src/*"],
"@ui8kit/hooks": ["../../packages/@ui8kit/hooks/src"],
"@ui8kit/hooks/*": ["../../packages/@ui8kit/hooks/src/*"],
"@ui8kit/form": ["../../packages/@ui8kit/form/src"],
"@ui8kit/form/*": ["../../packages/@ui8kit/form/src/*"],
"@ui8kit/flow": ["../../packages/@ui8kit/flow/src"],
"@ui8kit/flow/*": ["../../packages/@ui8kit/flow/src/*"],
"@ui8kit/chat": ["../../packages/@ui8kit/chat/src"],
"@ui8kit/chat/*": ["../../packages/@ui8kit/chat/src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}Create apps/dash/vite.config.ts:
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from 'path'
export default defineConfig(({ mode }) => {
// loadEnv is imported but not used in this configuration, which is acceptable.
// The 'mode' parameter is also not directly used in the returned config object, which is fine.
return {
plugins: [react()],
resolve: {
preserveSymlinks: true,
dedupe: ['react', 'react-dom'],
alias: {
'@': path.resolve(__dirname, './src'),
'@ui8kit/core': path.resolve(__dirname, '../../packages/@ui8kit/core/src'),
'@ui8kit/theme': path.resolve(__dirname, '../../packages/@ui8kit/theme/src'),
'@ui8kit/hooks': path.resolve(__dirname, '../../packages/@ui8kit/hooks/src'),
'@ui8kit/form': path.resolve(__dirname, '../../packages/@ui8kit/form/src'),
'@ui8kit/flow': path.resolve(__dirname, '../../packages/@ui8kit/flow/src'),
'@ui8kit/chat': path.resolve(__dirname, '../../packages/@ui8kit/chat/src'),
}
},
server: { port: 5000 }
})(),
})Create apps/dash/src/main.tsx:
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)Create a simple Hello World using @ui8kit/core/Button at apps/dash/src/App.tsx:
import { Button } from '@ui8kit/core'
export default function App() {
return (
<div style={{ padding: 24 }}>
<h1>Hello World</h1>
<Button>Click me</Button>
</div>
)
}Create apps/dash/index.html:
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dash</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>From the monorepo root:
bun install
# If submodule dir is empty after clone:
git submodule update --init --recursive
# Start all dev tasks (Turbo)
bun run dev
# Or start only the dash app
bun run dev:dashOpen http://localhost:5000 and you should see a page with a "Hello World" title and an @ui8kit/core <Button>.
- The submodule is used as if it were a regular directory inside the repo.
- Vite resolves the sources directly with
preserveSymlinks: trueand alias mapping to../../packages/@ui8kit/*/src. - You can import components like normal:
import { Button } from '@ui8kit/core'- From the app, the submodule path is reachable via
../../packages/@ui8kit/*. From the repo root, it is./packages/@ui8kit/*.
- Update submodule to the latest on its default branch:
cd packages/@ui8kit
git fetch
git checkout main
git pull --ff-only
cd ../../
git add packages/@ui8kit
git commit -m "chore(submodule): bump @ui8kit to latest"- Pull monorepo and update submodules to recorded commits:
git pull --recurse-submodules
git submodule update --init --recursive- See submodule status:
git submodule status --recursiveIf you need to remove packages/@ui8kit completely from Git tracking and your working tree:
# 1) De-initialize from Git config
git submodule deinit -f packages/@ui8kit
# 2) Remove the submodule entry from the index and working tree
git rm -f packages/@ui8kit
# 3) Remove the internal Git data for the submodule
rm -rf .git/modules/packages/@ui8kit/
# 4) (Optional) Clean any remaining references in .gitmodules (if present)
sed -i.bak "/packages\/@ui8kit/,+2d" .gitmodules || true
# Commit the removal
git commit -m "chore: remove @ui8kit submodule"What this accomplishes:
- Removed the submodule config from
.git/config(via deinit) - Deleted
.git/modules/packages/@ui8kit/(internal Git data) - Cleaned the Git index (removed submodule entry)
- Removed the working directory
packages/@ui8kit(viagit rm)
After this, git submodule update --init --recursive will work again, and the submodule is fully removed from version control.
- Clone correctly: Prefer
git clone --recurse-submodulesto avoid empty submodule directories. - Commit the pointer: The parent repo tracks a specific commit of the submodule. When you update the submodule, commit the updated pointer in the parent repo.
- Avoid duplicate React copies: Ensure Vite
resolve.dedupeincludesreactandreact-dom, and setpreserveSymlinks: true. - Use workspace ranges: In the app
package.json, depend on@ui8kit/*asworkspace:*to use local sources directly. - Align TS and Vite resolution: Mirror TS
pathsand Vitealiasso IDE and bundler resolve the same sources. - CI: Add
git submodule update --init --recursivebefore install/build steps. - Security: Submodules reference external repos. Pin to trusted branches/commits and review updates.
- Reproducibility: Use fixed Bun version (
bun@1.1.30) and leverage Turbo for caching.
# 0) New repo
mkdir your-monorepo && cd your-monorepo && git init
# 1) Add configs (root package.json, turbo.json, tsconfig.json)
# Create apps/dash and packages directories
# 2) Add submodule
git submodule add https://github.com/buildy-ui/ui8kit.git packages/@ui8kit
git submodule update --init --recursive
# 3) Create Vite app files under apps/dash (package.json, tsconfig.json, vite.config.ts, index.html, src/*)
# 4) Install and run
bun install
bun run dev:dashGPL-3.0