Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
••••••••••••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

Turborepo + Vite with React Monorepo (Part 2)

This is Part 2 of a two-part series. Part 1 covered the migration itself — moving 500+ files to a pnpm monorepo, fixing CSS resolution, and debugging a F…

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

This is Part 2 of a two-part series. Part 1 covered the migration itself — moving 500+ files to a pnpm monorepo, fixing CSS resolution, and debugging a Firebase singleton bug that only appears under pnpm's strict module isolation.



This post covers what happens after migration: integrating feature branches that don't know the monorepo exists, adding a second app under a sub-path, and the git merge strategy that turned 45 conflicts into zero.









The Problem: Three Branches, One Migration



After the migration landed on chore/monorepo-migration, we had two feature branches still built against the old flat src/ structure:





  1. feat/marketplace-integration — A new marketplace app with 14 commits, including its own (broken) monorepo migration attempt


  2. feat/automated-outreach — A 110-file, 18-commit feature built entirely against src/



Both needed to land on the migrated branch. Both had files that overlapped with each other and with the base. This is where things got interesting.









Adding a Second App to the Monorepo



The marketplace was a separate React app that needed to live under a sub-path: example.com/marketplace. Same domain as the dashboard, different app.






The workspace setup






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









apps/
├── dashboard/ # localhost:3002
└── marketplace/ # localhost:3003, hosted at /marketplace









// apps/marketplace/package.json
{
"name": "@acme/marketplace",
"scripts": {
"dev": "vite --port 3003"
}
}









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






turbo dev starts both apps in parallel. Simple enough. The complexity is in sub-path hosting.






The five layers of sub-path configuration



Sub-path hosting means the marketplace is served at /marketplace instead of /. This requires five things to agree, and missing any one produces a blank white screen with no error message.



Layer 1: Vite base




// apps/marketplace/vite.config.js
export default defineConfig({
base: "/marketplace/",
server: { port: 3003 },
});






This tells Vite to prefix all asset URLs with /marketplace/. Without it, the app tries to load main.js from / instead of /marketplace/main.js.



Layer 2: React Router basename




// apps/marketplace/src/index.jsx
<BrowserRouter basename="/marketplace">
<App />
</BrowserRouter>






Without this, the router doesn't recognize /marketplace/some-page as a valid route. All navigation fails silently.



Layer 3: Dev proxy




// apps/dashboard/vite.config.js
server: {
proxy: {
"/marketplace": {
target: "http://localhost:3003",
changeOrigin: true,
},
},
}






During development, the dashboard runs on port 3002. When a request comes in for /marketplace/*, the proxy forwards it to the marketplace dev server on port 3003.



Layer 4: The trailing slash



This one cost us 2 hours.



The browser requests /marketplace (no trailing slash). The proxy matches and forwards to port 3003. But the marketplace's Vite server expects /marketplace/ (with trailing slash) because that's its base. The HTML never loads. Blank screen. No error.



The fix — a 12-line Vite plugin:




function marketplaceRedirect() {
return {
name: "marketplace-redirect",
configureServer(server) {
server.middlewares.use((req, _res, next) => {
if (req.url === "/marketplace") {
req.url = "/marketplace/";
}
next();
});
},
};
}






One character — / — was the difference between a working app and a blank screen.



Layer 5: Asset references in index.html




<!-- apps/marketplace/index.html -->
<link rel="icon" href="/marketplace/favicon.png" />






Not /favicon.png. Not favicon.png. Must match the base path exactly. The favicon is the easy one to spot. The harder ones are Open Graph images, manifest files, and any hardcoded asset paths in your HTML.






The debugging principle



Sub-path apps fail silently. When something is misconfigured:




  • No 404 page (the proxy catches everything)

  • No console error (the HTML loads but contains nothing useful)

  • No network error (requests succeed, they just return the wrong content)



You get a blank white screen and have to check all five layers manually. We now have a checklist:




□ Vite base matches sub-path (with trailing slash)
□ React Router basename matches sub-path (without trailing slash)
□ Dashboard proxy rule exists for sub-path
□ Trailing slash redirect plugin is active
□ All index.html asset paths use the sub-path prefix
□ Hard refresh works (not just client-side navigation)












Integrating Feature Branches: The Wrong Way



With the marketplace working on the monorepo branch, we needed to bring in the outreach feature from feat/automated-outreach — 110 files, all referencing src/ paths.






What we tried first




  1. Checked out feat/automated-outreach

  2. Ran the migration script on it (moved files to apps/dashboard/src/)

  3. Committed the migration

  4. Merged feat/marketplace-integration into it



Result: 45 merge conflicts.



Both branches had independently run the migration script. Git saw every file that was moved from src/ to apps/dashboard/src/ as a conflict — because both branches created the same file at the same destination, even when the content was identical.






The resolution that silently broke everything



Under time pressure, we resolved all 45 conflicts by bulk-checking out files from the marketplace branch:




# "Quick" conflict resolution
git checkout feat/marketplace-integration -- packages/ui/src/components/button.jsx
git checkout feat/marketplace-integration -- packages/ui/src/components/card.jsx
git checkout feat/marketplace-integration -- apps/dashboard/src/features/campaign/constants/index.js
# ... 42 more files






The app started. No conflict markers. Looked clean.



Then at runtime:




SyntaxError: does not provide an export named 'getOutreachStatus'









What actually happened



git checkout <branch> -- <file> replaces the entire file with that branch's version. For shared UI components (button.jsx, card.jsx), this was fine — identical in both branches.



But campaign/constants/index.js was different. The outreach branch had added:




export const OUTREACH_AUTOMATION_STATUS = {
NOT_STARTED: "not_started",
ACTIVE: "active",
PAUSED: "paused",
};

export const OUTREACH_REACHOUT_STATUS = {
yet_to_reachout: {
label: "Yet to Reachout",
color: "text-slate-500",
},
reached_out_no_reply: {
label: "Reached Out - No Reply",
color: "text-amber-500",
},
// ...
};

export const getOutreachStatus = (status) =>
OUTREACH_REACHOUT_STATUS[status] ||
OUTREACH_REACHOUT_STATUS.yet_to_reachout;






The marketplace branch had the older version without these exports. By checking out the marketplace version, we silently deleted the outreach code. No warning. No error at merge time. The imports compiled fine because the file existed — it just didn't export what the outreach components expected.



We found similar silent deletions in:




  • Store file — outreach pagination state gone

  • Hooks file — outreach board columns gone

  • Creator list component — column management drawer gone

  • Workflow labels — outreach-specific labels gone



Total: 26 files with silently dropped code. All discovered only at runtime.






The lesson



Never bulk-resolve merge conflicts by checking out one side. It's the git equivalent of deleting files you haven't read. The correct approach depends on which branch owns each file:
























Conflict type Resolution
File exists only in branch A Take A's version
File exists only in branch B Take B's version
File modified in both branches Read both versions, merge manually


And after resolving, always diff against both parents:




# Did we lose anything from the outreach branch?
git diff HEAD -- apps/dashboard/src/features/campaign/

# Did we lose anything from the marketplace branch?
git diff feat/marketplace-integration -- packages/ui/












Integrating Feature Branches: The Right Way



We scrapped the broken merge and started over. The approach that works:






Step 1: Start from the migrated base






git checkout -b feat/automated-outreach upstream/chore/monorepo-migration






This gives us the clean monorepo structure — apps/dashboard/, packages/ui/, all the Vite config — without any feature code.






Step 2: Get the feature PR's final file state



Instead of cherry-picking commits (which carry merge history and conflict potential), we extract the final state of every changed file:




# Fetch the PR
git fetch upstream pull/XXXX/head:pr-ref

# List all files the PR changed
gh pr diff XXXX --name-only
# → src/features/campaign/api/mutations/createProposalMutation.js
# → src/features/campaign/api/mutations/index.js
# → src/features/campaign/constants/index.js
# → ... (110 files total)









Step 3: Copy with path mapping



Every file in the PR uses src/ paths. We need them under apps/dashboard/src/:




gh pr diff XXXX --name-only | while read f; do
[ "$f" = "package.json" ] && continue
target="apps/dashboard/$f"
mkdir -p "$(dirname "$target")"
git show pr-ref:"$f" > "$target"
done






This copies the final version of each file — all 18 commits squashed into the end result — and places it in the monorepo structure.






Step 4: Fix imports



The copied files still have old-style imports. Run the verification script:




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

# Revert 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' \
{} +









Step 5: Add new dependencies



The outreach feature introduced three new packages:




pnpm add --filter @acme/dashboard react-markdown remove-markdown turndown









Step 6: Commit and merge






git add apps/dashboard/ pnpm-lock.yaml
git commit -m "feat: add automated outreach feature to monorepo"

# Now merge the marketplace branch
git merge feat/marketplace-integration --no-edit






Result: zero conflicts.



The outreach commit added and modified files in apps/dashboard/src/features/campaign/. The marketplace branch added apps/marketplace/ and dashboard proxy config. No overlapping files.






Why this works



The key insight: don't migrate the feature branch. Apply the feature's changes directly onto the already-migrated base.



When you migrate two branches independently:




Base → Migration → Branch A (all files moved to apps/dashboard/)
Base → Migration → Branch B (all files moved to apps/dashboard/)
Merge A + B → 45 conflicts (same files created at same paths)






When you apply features onto the migrated base:




Base → Migration → Feature A applied → Feature B merged → 0 conflicts






The second approach treats migration as infrastructure and features as content. Infrastructure is shared (one copy). Content is separate (no overlap).









The Import Safety Net



Throughout this process — cherry-picks, merges, file copies — old-style imports kept reappearing. Every time code from a pre-migration branch enters the monorepo, some files will have:




import { Button } from "~/components/ui/button"; // broken






Instead of:




import { Button } from "@acme/ui/components/button"; // correct






We ran Step 24 of the migration script five times over the course of the integration. It's designed to be run repeatedly:




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
# Fix and report
fi






Running it on already-fixed code is a no-op. Running it after a merge catches anything that slipped through. It's the cheapest safety net we have.




Treat migration as a gate, not a one-time event. Until every pre-migration branch is merged, this gate needs to exist.










The Complete Workflow



Here's the process we now follow for integrating any pre-migration feature branch:




# 1. Start from the migrated base
git checkout -b feat/my-feature upstream/chore/monorepo-migration

# 2. Fetch and extract the feature PR
git fetch upstream pull/XXXX/head:pr-ref
gh pr diff XXXX --name-only | while read f; do
target="apps/dashboard/$f"
mkdir -p "$(dirname "$target")"
git show pr-ref:"$f" > "$target"
done

# 3. Fix imports
# (run Step 24)

# 4. Add new dependencies
pnpm add --filter @acme/dashboard <new-packages>

# 5. Commit
git add apps/dashboard/ pnpm-lock.yaml
git commit -m "feat: add <feature> to monorepo"

# 6. Merge other branches (zero conflicts expected)
git merge feat/other-branch --no-edit

# 7. Run Step 24 again (the merge may have brought old imports)

# 8. Verify
grep -r '~/components/ui/' apps/dashboard/src/ \
--include='*.jsx' --include='*.js' \
| grep -v 'async-select\|confirm-dialog'
# Should return nothing












What We Learned






Things that broke silently






































Issue Symptom Time to diagnose
CSS imports ignoring exports
ENOENT on shared stylesheet 30 min
Firebase singleton under pnpm "Service database is not available" 4 hours
Trailing slash in sub-path Blank white screen, no errors 2 hours

git checkout --theirs dropping code
Missing exports at runtime 3 hours
Old imports after merge Vite import analysis failure 10 min (once we had Step 24)





Things that could have been caught earlier




  1. A pre-migration CI check: If we'd added grep -r '~/components/ui/' apps/dashboard/src/ to CI, broken imports would fail the build instead of reaching dev.


  2. Firebase integration test: A simple test that calls getDatabase(app) would have caught the singleton issue immediately instead of us discovering it in the browser.


  3. Sub-path smoke test: A curl to localhost:3002/marketplace checking for a 200 with non-empty body would have caught the trailing slash issue in seconds.







The production-level principles




  1. pnpm breaks singleton libraries. Add resolve.dedupe for any library that shares state between sub-packages. This is invisible until runtime.


  2. CSS tooling lives outside the module system. Use Vite aliases for shared packages that contain styles. exports won't help you.


  3. Sub-path apps fail silently. Five layers must agree. Build a checklist and test with hard refresh.


  4. Never bulk-resolve git conflicts. Read both sides. Diff against both parents after resolving.


  5. Migration is ongoing. Until every old-structure branch is merged, keep the import verification script and run it after every merge.


  6. Apply features onto the migrated base, not the other way around. This single decision turned 45 conflicts into zero.










Final State






my-app/
├── apps/
│ ├── dashboard/ # @acme/dashboard (port 3002)
│ └── marketplace/ # @acme/marketplace (port 3003, /marketplace)
├── packages/
│ └── ui/ # @acme/ui (shared shadcn/ui components)
├── scripts/
│ └── migrate-to-monorepo.sh
├── pnpm-workspace.yaml
├── turbo.json
└── package.json






One command starts everything:




$ pnpm dev






Two apps running, shared components, Firebase working, all feature branches integrated. The migration touched 500+ files across five branches. The final commit history is clean. The process is documented. And we have a script that catches the next engineer's old-style imports before they reach production.



That's the part the tutorials leave out — the migration isn't done when the script finishes. It's done when the last pre-migration branch merges and the safety net can finally be removed.

2. Cyber Threat Intelligence & Forensik

CTI Threat Relationship Graph2 Knoten / 1 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
Ähnliche Beiträge
🔍 Verwandte News

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

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