Table of Contents
- Background
- Structure Overview
- Vite Setup
- Step 1: Importing a TypeScript Utility Function
- Step 2: Importing a Hook
- Step 3: Importing a React Component
- Step 4: Importing a Client-Only Component
- Step 5: Importing a Side-Effect Component
- Bundle Optimization
- Conclusion
Background
At our company, we adopted a monorepo setup without any monorepo toolingβno Nx, Turborepo, or even pnpm workspaces.
Instead, we took the minimalist approach: local npm install
between packages.
Frameworks in Our Setup
- Host apps: Next.js 12, Next.js 14 (referred to as 12+), and Vite
- Shared library: Built using Vite
Our setup successfully supports:
- β Hot reloading with a single dev server
- β Tree-shaking in the final bundle (works for Next.js 12+ and Vite only)
The shared library exports four kinds of features (which I call "stuffs" π):
- UI components (built with
shadcn/ui
) - Utility functions (TypeScript functions and React hooks)
- Client-only components (e.g.
window.location.hostname
) - Side-effect components (e.g.
d3-transition
)
These represent a good mix of typical front-end use cases: from pure TS functions to client-only components and side-effect-heavy modules.
In this post, I'll show you the exact setup and pitfalls we encountered β without using monorepo tooling β and how we solved them.
This blog walks through our Vite host app setupβeach section progressively introduces new types of exports into the host, checking their behavior at every step:
tsc --noEmit
npm run dev
npm run build && npm run preview
Structure Overview
root
βββ next12app
βββ next14app
βββ viteapp
βββ shared
βββ src
β βββ @/components
β βββ client-only-component
β βββ side-effect-component
β βββ utils
β βββ index.ts # Barrel file
βββ tsconfig.json
βββ vite.config.ts
- The shared package exports all modules through a single barrel file (
index.ts
) package.json
in the shared folder has itsmain
field pointing tosrc/index.ts
- We'll refine this setup progressively as we hit roadblocks.
Vite Setup
root
βββ next12app
βββ next14app
βββ viteapp
β βββ src
β βββ tsconfig.json
β βββ vite.config.ts
βββ shared
βββ src
β βββ @/components
β βββ client-only-component
β βββ side-effect-component
β βββ utils
β βββ index.ts
The library is installed via:
npm install ../shared
Step 1: Importing a TypeScript Utility Function
import { add } from "shared";
Commands results:
tsc --noEmit
: βnpm run dev
: βnpm run build
: β
Step 2: Importing a Hook
import { useMediaQuery } from "shared";
Commands results:
tsc --noEmit
: βnpm run dev
: βnpm run build
: β
Step 3: Importing a React Component
import { Button } from "shared";
Commands results:
tsc --noEmit
: βnpm run dev
: βnpm run build
: β
Error:
Module not found: Can't resolve '@/lib/utils'
β Fix
Option 1: Convert to relative imports
// from
import { cn } from "@/lib/utils";
// to
import { cn } from "../../lib/utils";
Downside: Manual work for every new file or refactor.
Option 2: Add alias in vite.config.ts
resolve: {
alias: {
"@": path.resolve(__dirname, "../shared/src/@"),
}
}
Downside: Tight coupling to folder structure + host app can't reuse the @
alias.
β‘οΈ We'll go with Option 2 for now.
Commands results:
tsc --noEmit
: βnpm run dev
: βnpm run build
: β
Error:
TypeError: Cannot read properties of null (reading 'useState')
Fix: Explicitly alias react
resolve: {
alias: {
"@": path.resolve(__dirname, "../shared/src/@"),
react: path.resolve(__dirname, "./node_modules/react"),
}
}
β Everything works again.
Step 4: Importing a Client-Only Component
import { ClientOnlyComponent } from "shared";
Commands results:
tsc --noEmit
: βnpm run dev
: βnpm run build
: β
Step 5: Importing a Side-Effect Component
import { SideEffectComponent } from "shared";
Commands results:
tsc --noEmit
: βnpm run dev
: βnpm run build
: β
Note: If youβre using d3-selection
with transition
, remember to import side-effects:
import "d3-transition";
Otherwise you'll see:
TypeError: .transition is not a function
Interestingly, tsc
doesnβt catch this π€·
Bundle Optimization
Initial output: 378.34 kB

Multiple react-dom
copies detected.
Fix it by aliasing:
resolve: {
alias: {
"@": path.resolve(__dirname, "../shared/src/@"),
react: path.resolve(__dirname, "./node_modules/react"),
"react-dom": path.resolve(__dirname, "./node_modules/react-dom"),
}
}
New output: 240.97 kB
Further improvement: Tree-shake d3-transition
using:
"sideEffects": ["**/node_modules/d3-transition"]
Final output: 206.92 kB

Conclusion
- Vite's dev server works out of the box and feels lightweight
- Manual config is needed for tree-shaking and alias resolution
- Still, a clean setup is achievable without monorepo tooling