Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
YouTube Security VideosNutanix advances legacy and AI app management with AMD(01.10.2026 um 16:00 Uhr)
•
YouTube Security VideosPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••••
Videos & KonferenzenPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••
Sicherheitslücken (CVE)USN-8857-1: KCoreAddons vulnerability(01.10.2026 um 12:48 Uhr)
•••
YouTube Security VideosNutanix advances legacy and AI app management with AMD(01.10.2026 um 16:00 Uhr)
•
YouTube Security VideosPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••••
Videos & KonferenzenPC-WELT: 32 TB SSD-Speicher in der HMX 6!(01.10.2026 um 16:15 Uhr)
••
Sicherheitslücken (CVE)USN-8857-1: KCoreAddons vulnerability(01.10.2026 um 12:48 Uhr)
•••
Intelligence View
⚡ tsecurity.de Intelligence

Turborepo + Vite with React Monorepo (Part 1)

This is Part 1 of a two-part series on migrating a production React app to a monorepo. This post covers the migration itself — the script, the structure d…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

This is Part 1 of a two-part series on migrating a production React app to a monorepo. This post covers the migration itself — the script, the structure decisions, and two invisible runtime bugs that only appear after you switch package managers. Part 2 covers integrating feature branches, adding a new sub-path app, and the merge strategy that saved us from 45-file conflict hell.









The Codebase



A production SaaS platform — React 18, Vite, Tailwind CSS, shadcn UI, Firebase Realtime Database, React Query, Zustand. Over 500 files, 30+ shadcn UI components, active development by multiple engineers.



Everything lived under one src/ directory:




acme-dashboard/
├── src/
│ ├── components/
│ │ ├── ui/ ← 30+ shadcn components
│ │ ├── AppSidebar/
│ │ ├── ProfileView/
│ │ └── ...
│ ├── features/ ← Feature modules
│ ├── pages/
│ ├── hooks/
│ ├── stores/
│ ├── contexts/
│ └── configs/
│ └── firebase-config.js
├── public/
├── vite.config.js
└── package.json






This worked fine until a second app needed the same shadcn components. Copy-pasting 30+ UI components across repos and keeping them in sync wasn't sustainable. We needed a shared package.









Why pnpm + Turborepo



We evaluated three setups:




























Setup Pros Cons
npm workspaces Zero config, familiar Slow installs, phantom dependencies from hoisting
yarn berry (PnP) Fast, strict Breaks many packages, IDE support is flaky
pnpm workspaces + Turborepo Fast installs, strict isolation, task orchestration Singleton issues (we'll get to this)


pnpm won because of disk efficiency (content-addressable store), strict module isolation (no phantom dependencies), and speed. Turborepo added parallel task execution and caching on top.



The trade-off we didn't know about yet: pnpm's strict isolation would break Firebase in a way that produces zero warnings at install time and only fails at runtime. More on that later.









The Target Structure






acme-dashboard/
├── apps/
│ └── dashboard/ # @acme/dashboard
│ ├── src/
│ │ ├── components/ # Dashboard-specific components
│ │ ├── features/
│ │ ├── pages/
│ │ └── ...
│ ├── vite.config.js
│ └── package.json
├── packages/
│ └── ui/ # @acme/ui
│ ├── src/
│ │ ├── components/ # Shared shadcn components
│ │ ├── styles/
│ │ └── lib/utils.js
│ └── package.json
├── scripts/
│ └── migrate-to-monorepo.sh
├── pnpm-workspace.yaml
├── turbo.json
└── package.json # Root workspace config






Two decisions were made upfront:




  1. Only shadcn UI components move to the shared package. Feature components, pages, stores — everything else stays in apps/dashboard/. Extracting too much creates coupling between the shared package and app-specific code.


  2. Two components stay in dashboard even though they're in components/ui/. async-select depends on React Query hooks. confirm-dialog uses the app's toast system. Moving them would pull the entire app into packages/ui as a dependency.










The Migration Script



We wrote a 24-step bash script. Not because bash is the right tool — but because the migration is a one-time operation, and a script makes it reproducible. When three feature branches all need migration, running the same script ensures consistency.






Phase 1: Scaffold (Steps 1–10)






# Create workspace structure
mkdir -p apps/dashboard packages/ui/src/{components,styles,lib,hooks}

# Move app files
mv src/ apps/dashboard/src/
mv public/ apps/dashboard/public/
mv vite.config.js apps/dashboard/
mv index.html apps/dashboard/
# ... tailwind.config, postcss.config, etc.






Generate workspace config:




# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"









// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": { "persistent": true, "cache": false },
"build": { "dependsOn": ["^build"], "outputs": ["build/**"] },
"format": {},
"lint": {}
}
}









Phase 2: Extract Shared UI (Steps 11–20)



This is where the decisions matter. We move shadcn components but need to handle their dependencies:




# Move components
mv apps/dashboard/src/components/ui/* packages/ui/src/components/
# Keep the two that depend on app code
mv packages/ui/src/components/async-select.jsx apps/dashboard/src/components/ui/
mv packages/ui/src/components/confirm-dialog.jsx apps/dashboard/src/components/ui/

# Move shared utilities
mv apps/dashboard/src/lib/utils.js packages/ui/src/lib/
mv apps/dashboard/src/styles/global.css packages/ui/src/styles/






The shared package's package.json:




{
"name": "@acme/ui",
"private": true,
"type": "module",
"exports": {
"./components/*": "./src/components/*",
"./styles/*": "./src/styles/*",
"./lib/*": "./src/lib/*"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}






We defined exports for correctness, but — spoiler — they won't be what actually resolves the imports. More on that in the next section.






Phase 3: Rewrite Imports (Steps 21–23)



Every file that imported from ~/components/ui/ needs to point to @acme/ui/components/:




find apps/dashboard/src -type f \( -name "*.jsx" -o -name "*.js" \) -exec \
sed -i '' \
-e 's|from "~/components/ui/\([^"]*\)"|from "@acme/ui/components/\1"|g' \
-e "s|from '~/components/ui/\([^']*\)'|from '@acme/ui/components/\1'|g" \
{} +

# Revert the two excluded components
find apps/dashboard/src -type f \( -name "*.jsx" -o -name "*.js" \) -exec \
sed -i '' \
-e 's|from "@acme/ui/components/async-select"|from "~/components/ui/async-select"|g' \
-e 's|from "@acme/ui/components/confirm-dialog"|from "~/components/ui/confirm-dialog"|g' \
{} +









Phase 4: Verify (Step 24)



This step exists because imports will break again — every cherry-pick, merge, or branch integration can reintroduce old-style imports:




BROKEN=$(find apps/dashboard/src -type f \( -name "*.jsx" -o -name "*.js" \) \
-exec grep -l 'from "~/components/ui/' {} + 2>/dev/null | \
xargs grep 'from "~/components/ui/' 2>/dev/null | \
grep -v "async-select" | grep -v "confirm-dialog" || true)

if [ -n "$BROKEN" ]; then
warn "Found broken ~/components/ui/ imports — fixing..."
# ... same sed replacement as above
fi






This step is idempotent. Running it on already-fixed code changes nothing. We ended up running it five more times over the next week as branches merged in.









The First Invisible Bug: CSS Imports Don't Resolve Exports



After running the script and pnpm install, we started the dev server:




[postcss] ENOENT: no such file or directory, open '@acme/ui/styles/globals.css'






The import in apps/dashboard/src/styles/global.css:




@import "@acme/ui/styles/globals.css";






We'd set up exports in packages/ui/package.json to handle this. But CSS @import statements don't resolve through Node.js exports fields. PostCSS and Vite's CSS pipeline bypass the module system entirely. They look for the file on disk, following the package's main field or the literal path — never exports.



This is a fundamental gap. The exports field was designed for JavaScript modules. CSS tooling predates it and has no integration with it.






The fix



Skip exports for resolution and use a Vite alias:




// apps/dashboard/vite.config.js
resolve: {
alias: [
{ find: "~", replacement: resolve(__dirname, "./src") },
{ find: "@acme/ui", replacement: resolve(__dirname, "../../packages/ui/src") },
],
}






The alias resolves @acme/ui/styles/globals.css to ../../packages/ui/src/styles/globals.css before the CSS pipeline ever sees it. Works for JS imports, CSS imports, and dynamic imports.



We kept exports in the package.json for documentation purposes, but the alias is what actually resolves everything.



The thing that trips people up: this only matters for packages that contain non-JS assets (CSS, images, fonts). Pure JS packages work fine with exports. The moment you share styles across workspaces, you need the alias.









The Second Invisible Bug: pnpm Breaks Firebase



The CSS fix let the dev server start. The app loaded to a white screen with this in the console:




Uncaught Error: Service database is not available
at firebase-config.js:46:27






Line 46: export const realTimeDB = getDatabase(app);



Same code, same Firebase version, same config. The only change was the package manager.






The debugging journey



We started with the obvious — is firebase/database installed? Yes. Is app initialized? Yes. Is getDatabase imported correctly? Yes.



We added console logs:




const app = initializeApp(firebaseConfig);
console.log("App:", app.name); // "[DEFAULT]" ✓
console.log("App options:", app.options); // Full config ✓
const db = getDatabase(app); // 💥 Error






The app was initialized. The database just couldn't see it.






How pnpm isolates modules



npm and yarn use a flat node_modules/. Every package can see every other package. pnpm uses a virtual store — each package gets its own isolated dependencies:




node_modules/
├── .pnpm/
│ ├── @[email protected]/
│ │ └── node_modules/
│ │ └── @firebase/app/ ← copy A
│ └── @[email protected]/
│ └── node_modules/
│ └── @firebase/database/ ← can't see copy A
│ └── (no @firebase/app here)
└── @firebase/app/ ← hoisted symlink






@firebase/database needs @firebase/app but doesn't declare it as a direct dependency (it's a peer dependency expected to be hoisted). In npm/yarn, hoisting makes it available everywhere. In pnpm, the package at .pnpm/@[email protected]/ can't traverse up to find the hoisted copy.






What Vite does with this



Vite's dev server resolves imports on the fly. When @firebase/database internally imports @firebase/app, Vite starts resolution from the database package's location in .pnpm/. It doesn't find @firebase/app there, so it tries... and either finds a different copy or fails silently by creating a new instance.



The result: two separate @firebase/app instances. Your code registers the Firebase app in instance A. @firebase/database looks for it in instance B. The registry is empty. "Service not available."



We confirmed this by checking the actual paths:




# Where does @firebase/database live?
realpath node_modules/.pnpm/@[email protected]/node_modules/@firebase/database
# → /Users/.../node_modules/.pnpm/@[email protected]/.../@firebase/database

# Can it see @firebase/app?
ls node_modules/.pnpm/@[email protected]/node_modules/@firebase/app
# → No such file or directory

# Where is @firebase/app actually?
realpath node_modules/@firebase/app
# → /Users/.../node_modules/.pnpm/@[email protected]/.../@firebase/app






Two completely isolated paths. No way for one to find the other.






The fix



Two Vite config options, both required:




// apps/dashboard/vite.config.js
resolve: {
dedupe: ["@firebase/app", "@firebase/component"],
},
optimizeDeps: {
include: [
"firebase/app",
"firebase/auth",
"firebase/database",
],
},






resolve.dedupe: Forces Vite to always resolve @firebase/app and @firebase/component from the project root, regardless of where the import originates. One copy, one singleton, one registry.



optimizeDeps.include with firebase/database: Forces Vite to pre-bundle the database module during dev server startup. Pre-bundling respects the dedupe setting, so the pre-bundled database uses the same @firebase/app instance as everything else.



Without both, it doesn't work:




























Config Result
Only optimizeDeps.include
Pre-bundles each module separately → separate instances
Only resolve.dedupe
Dev server bypasses pre-bundling, resolves from .pnpm directly
optimizeDeps.exclude: ["firebase"] No pre-bundling at all → .pnpm resolution → broken
Neither Same as before migration broke it





This isn't just a Firebase problem



Any library that uses a global singleton pattern will break under pnpm + Vite if the internal packages don't declare proper peer dependencies. Libraries we've seen this affect:




  • Firebase (all services)

  • Some analytics SDKs that use a shared event bus

  • Auth libraries with global token stores

  • Any library where sub-packages need to share state via a common core



The fix is always the same: resolve.dedupe the core package + include the sub-packages in optimizeDeps.









Running the Migration



The complete migration checklist:




# 1. Run the script
bash scripts/migrate-to-monorepo.sh

# 2. Install dependencies (creates workspace symlinks)
pnpm install

# 3. Verify the Vite config has:
# - @acme/ui alias
# - Firebase dedupe
# - Firebase in optimizeDeps.include

# 4. Start dev server
pnpm dev

# 5. Verify:
# - App loads without white screen
# - Firebase Realtime DB works (no "Service not available")
# - All UI components render (no broken imports)
# - CSS styles apply (shared globals.css loads)






If step 4 shows errors, the most common causes:





  • ENOENT on @acme/ui: Run pnpm install — the workspace symlink doesn't exist yet


  • "Service X is not available": Add the Firebase service to resolve.dedupe and optimizeDeps.include


  • Missing component: Run Step 24 again — an old import snuck through


  • CSS not loading: Check the Vite alias points to the right relative path









What We Ended Up With






// Root package.json
{
"scripts": {
"dev": "turbo dev",
"dev:dashboard": "pnpm --filter @acme/dashboard dev",
"build": "turbo build",
"format": "turbo format"
}
}









$ pnpm dev
# Starts dashboard on localhost:3002
# Turborepo orchestrates all workspace tasks in parallel






The dashboard works exactly as before — same features, same routes, same behavior. But now packages/ui is a shared dependency that any new app can import from. And that's exactly what we needed for Part 2, where we add a second app, integrate a 110-file feature branch, and learn why you should never merge two independently-migrated branches.






Next: Part 2 — Feature Branches, Sub-Path Apps, and the Merge Strategy Nobody Tells You About

🔍 CTI & Forensik

Cyber Threat Intelligence & Forensik

Bedrohungsgraph · ATT&CK-Mapping · Exploit-Belege
CTI Threat Relationship Graph
Akteure · Techniken · Beziehungen
3 Knoten · 2 Relationen
CVE / Incident Threat Actor Software MITRE ATT&CK CWE Weakness IoC
MITRE ATT&CK Matrix Navigator
Enterprise-Matrix · nur belegte Techniken
14 Taktiken
1 belegte Technik
T1059TA0002 · Execution
Command and Scripting Interpreter
Mitigation: M1038 Execution Prevention & Script Block Logging
Quelle: Kontext-Klassifikation des Artikeltextes
Reconnaissance
Resource Development
Initial Access
Execution
Persistence
Privilege Escalation
Defense Evasion
Credential Access
Discovery
Lateral Movement
Collection
Command and Control
Exfiltration
Impact
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Turborepo + Vite with React Monorepo (Part 1)

Thematisch verwandte Begriffe: Turborepo, Vite, with, React · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag