From e50f59062fa9ab26813c4822bf58bb823cf15e80 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 13 Feb 2026 14:25:26 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=8E=EF=B8=8F=20feat:=20Smart=20Reinsta?= =?UTF-8?q?ll=20with=20Turborepo=20Caching=20for=20Better=20DX=20(#11785)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Add Turborepo support and smart reinstall script - Updated .gitignore to include Turborepo cache directory. - Added Turbo as a dependency in package.json and package-lock.json. - Introduced turbo.json configuration for build tasks. - Created smart-reinstall.js script to optimize dependency installation and package builds using Turborepo caching. * fix: Address PR review feedback for smart reinstall - Fix Windows compatibility in hasTurbo() by checking for .cmd/.ps1 shims - Remove Unix-specific shell syntax (> /dev/null 2>&1) from cache clearing - Split try/catch blocks so daemon stop failure doesn't block cache clear - Add actionable tips in error output pointing to --force and --verbose --- .gitignore | 3 + config/smart-reinstall.js | 235 ++++++++++++++++++++++++++++++++++++++ package-lock.json | 103 +++++++++++++++++ package.json | 3 + turbo.json | 33 ++++++ 5 files changed, 377 insertions(+) create mode 100644 config/smart-reinstall.js create mode 100644 turbo.json diff --git a/.gitignore b/.gitignore index d0c87ff03d..86d4a3ddae 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ coverage config/translations/stores/* client/src/localization/languages/*_missing_keys.json +# Turborepo +.turbo + # Compiled Dirs (http://nodejs.org/api/addons.html) build/ dist/ diff --git a/config/smart-reinstall.js b/config/smart-reinstall.js new file mode 100644 index 0000000000..18fe689127 --- /dev/null +++ b/config/smart-reinstall.js @@ -0,0 +1,235 @@ +#!/usr/bin/env node +/** + * Smart Reinstall for LibreChat + * + * Combines cached dependency installation with Turborepo-powered builds. + * + * Dependencies (npm ci): + * Hashes package-lock.json and stores a marker in node_modules. + * Skips npm ci entirely when the lockfile hasn't changed. + * + * Package builds (Turborepo): + * Turbo hashes each package's source/config inputs, caches build + * outputs (dist/), and restores from cache when inputs match. + * Turbo v2 uses a global cache (~/.cache/turbo) that survives + * npm ci and is shared across worktrees. + * + * Usage: + * npm run smart-reinstall # Smart cached mode + * npm run smart-reinstall -- --force # Full clean reinstall, bust all caches + * npm run smart-reinstall -- --skip-client # Skip frontend (Vite) build + * npm run smart-reinstall -- --clean-cache # Wipe turbo build cache + * npm run smart-reinstall -- --verbose # Turbo verbose output + */ + +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Adds console.green, console.purple, etc. +require('./helpers'); + +// ─── Configuration ─────────────────────────────────────────────────────────── + +const ROOT_DIR = path.resolve(__dirname, '..'); +const DEPS_HASH_MARKER = path.join(ROOT_DIR, 'node_modules', '.librechat-deps-hash'); + +const flags = { + force: process.argv.includes('--force'), + cleanCache: process.argv.includes('--clean-cache'), + skipClient: process.argv.includes('--skip-client'), + verbose: process.argv.includes('--verbose'), +}; + +// Workspace directories whose node_modules should be cleaned during reinstall +const NODE_MODULES_DIRS = [ + ROOT_DIR, + path.join(ROOT_DIR, 'packages', 'data-provider'), + path.join(ROOT_DIR, 'packages', 'data-schemas'), + path.join(ROOT_DIR, 'packages', 'client'), + path.join(ROOT_DIR, 'packages', 'api'), + path.join(ROOT_DIR, 'client'), + path.join(ROOT_DIR, 'api'), +]; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function hashFile(filePath) { + return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex').slice(0, 16); +} + +function exec(cmd, opts = {}) { + execSync(cmd, { cwd: ROOT_DIR, stdio: 'inherit', ...opts }); +} + +// ─── Dependency Installation ───────────────────────────────────────────────── + +function checkDeps() { + const lockfile = path.join(ROOT_DIR, 'package-lock.json'); + if (!fs.existsSync(lockfile)) { + return { needsInstall: true, hash: 'missing' }; + } + + const hash = hashFile(lockfile); + + if (!fs.existsSync(path.join(ROOT_DIR, 'node_modules'))) { + return { needsInstall: true, hash }; + } + if (!fs.existsSync(DEPS_HASH_MARKER)) { + return { needsInstall: true, hash }; + } + + const stored = fs.readFileSync(DEPS_HASH_MARKER, 'utf-8').trim(); + return { needsInstall: stored !== hash, hash }; +} + +function installDeps(hash) { + const { deleteNodeModules } = require('./helpers'); + NODE_MODULES_DIRS.forEach(deleteNodeModules); + + console.purple('Cleaning npm cache...'); + exec('npm cache clean --force'); + + console.purple('Installing dependencies (npm ci)...'); + exec('npm ci'); + + fs.writeFileSync(DEPS_HASH_MARKER, hash, 'utf-8'); +} + +// ─── Turbo Build ───────────────────────────────────────────────────────────── + +function runTurboBuild() { + const args = ['npx', 'turbo', 'run', 'build']; + + if (flags.skipClient) { + args.push('--filter=!@librechat/frontend'); + } + + if (flags.force) { + args.push('--force'); + } + + if (flags.verbose) { + args.push('--verbosity=2'); + } + + const cmd = args.join(' '); + console.gray(` ${cmd}\n`); + exec(cmd); +} + +/** + * Fallback for when turbo is not installed (e.g., first run before npm ci). + * Runs the same sequential build as the original `npm run frontend`. + */ +function runFallbackBuild() { + console.orange(' turbo not found — using sequential fallback build\n'); + + const scripts = [ + 'build:data-provider', + 'build:data-schemas', + 'build:api', + 'build:client-package', + ]; + + if (!flags.skipClient) { + scripts.push('build:client'); + } + + for (const script of scripts) { + console.purple(` Running ${script}...`); + exec(`npm run ${script}`); + } +} + +function hasTurbo() { + const binDir = path.join(ROOT_DIR, 'node_modules', '.bin'); + return ['turbo', 'turbo.cmd', 'turbo.ps1'].some((name) => fs.existsSync(path.join(binDir, name))); +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +(async () => { + const startTime = Date.now(); + + console.green('\n Smart Reinstall — LibreChat'); + console.green('─'.repeat(45)); + + // ── Handle --clean-cache ─────────────────────────────────────────────── + if (flags.cleanCache) { + console.purple('Clearing Turborepo cache...'); + if (hasTurbo()) { + try { + exec('npx turbo daemon stop', { stdio: 'pipe' }); + } catch { + // ignore — daemon may not be running + } + } + // Clear local .turbo cache dir + const localTurboCache = path.join(ROOT_DIR, '.turbo'); + if (fs.existsSync(localTurboCache)) { + fs.rmSync(localTurboCache, { recursive: true }); + } + // Clear global turbo cache + if (hasTurbo()) { + try { + exec('npx turbo clean', { stdio: 'pipe' }); + console.green('Turbo cache cleared.'); + } catch { + console.gray('Could not clear global turbo cache (may not exist yet).'); + } + } else { + console.gray('turbo not installed — nothing to clear.'); + } + + if (!flags.force) { + return; + } + } + + // ── Step 1: Dependencies ─────────────────────────────────────────────── + console.purple('\n[1/2] Checking dependencies...'); + + if (flags.force) { + console.orange(' Force mode — reinstalling all dependencies'); + const lockfile = path.join(ROOT_DIR, 'package-lock.json'); + const hash = fs.existsSync(lockfile) ? hashFile(lockfile) : 'none'; + installDeps(hash); + console.green(' Dependencies installed.'); + } else { + const { needsInstall, hash } = checkDeps(); + if (needsInstall) { + console.orange(' package-lock.json changed or node_modules missing'); + installDeps(hash); + console.green(' Dependencies installed.'); + } else { + console.green(' Dependencies up to date — skipping npm ci'); + } + } + + // ── Step 2: Build packages ───────────────────────────────────────────── + console.purple('\n[2/2] Building packages...'); + + if (hasTurbo()) { + runTurboBuild(); + } else { + runFallbackBuild(); + } + + // ── Done ─────────────────────────────────────────────────────────────── + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(''); + console.green('─'.repeat(45)); + console.green(` Done (${elapsed}s)`); + console.green(' Start the app with: npm run backend'); + console.green('─'.repeat(45)); +})().catch((err) => { + console.red(`\nError: ${err.message}`); + if (flags.verbose) { + console.red(err.stack); + } + console.gray(' Tip: run with --force to clean all caches and reinstall from scratch'); + console.gray(' Tip: run with --verbose for detailed output'); + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json index 29c38184e2..402f4872e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "lint-staged": "^15.4.3", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", + "turbo": "^2.8.7", "typescript-eslint": "^8.24.0" } }, @@ -39915,6 +39916,108 @@ "dev": true, "license": "MIT" }, + "node_modules/turbo": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.7.tgz", + "integrity": "sha512-RBLh5caMAu1kFdTK1jgH2gH/z+jFsvX5rGbhgJ9nlIAWXSvxlzwId05uDlBA1+pBd3wO/UaKYzaQZQBXDd7kcA==", + "dev": true, + "license": "MIT", + "bin": { + "turbo": "bin/turbo" + }, + "optionalDependencies": { + "turbo-darwin-64": "2.8.7", + "turbo-darwin-arm64": "2.8.7", + "turbo-linux-64": "2.8.7", + "turbo-linux-arm64": "2.8.7", + "turbo-windows-64": "2.8.7", + "turbo-windows-arm64": "2.8.7" + } + }, + "node_modules/turbo-darwin-64": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.8.7.tgz", + "integrity": "sha512-Xr4TO/oDDwoozbDtBvunb66g//WK8uHRygl72vUthuwzmiw48pil4IuoG/QbMHd9RE8aBnVmzC0WZEWk/WWt3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-darwin-arm64": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.7.tgz", + "integrity": "sha512-p8Xbmb9kZEY/NoshQUcFmQdO80s2PCGoLYj5DbpxjZr3diknipXxzOK7pcmT7l2gNHaMCpFVWLkiFY9nO3EU5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/turbo-linux-64": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.8.7.tgz", + "integrity": "sha512-nwfEPAH3m5y/nJeYly3j1YJNYU2EG5+2ysZUxvBNM+VBV2LjQaLxB9CsEIpIOKuWKCjnFHKIADTSDPZ3D12J5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-linux-arm64": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.8.7.tgz", + "integrity": "sha512-mgA/M6xiJzyxtXV70TtWGDPh+I6acOKmeQGtOzbFQZYEf794pu5jax26bCk5skAp1gqZu3vacPr6jhYHoHU9IQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/turbo-windows-64": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.8.7.tgz", + "integrity": "sha512-sHTYMaXuCcyHnGUQgfUUt7S8407TWoP14zc/4N2tsM0wZNK6V9h4H2t5jQPtqKEb6Fg8313kygdDgEwuM4vsHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/turbo-windows-arm64": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.8.7.tgz", + "integrity": "sha512-WyGiOI2Zp3AhuzVagzQN+T+iq0fWx0oGxDfAWT3ZiLEd4U0cDUkwUZDKVGb3rKqPjDL6lWnuxKKu73ge5xtovQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", diff --git a/package.json b/package.json index 80dea27369..4f15a10e05 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "LibreChat", "version": "v0.8.2", "description": "", + "packageManager": "npm@11.10.0", "workspaces": [ "api", "client", @@ -15,6 +16,7 @@ "user-stats": "node config/user-stats.js", "rebuild:package-lock": "node config/packages", "reinstall": "node config/update.js -l -g", + "smart-reinstall": "node config/smart-reinstall.js", "b:reinstall": "bun config/update.js -b -l -g", "reinstall:docker": "node config/update.js -d -g", "update:local": "node config/update.js -l", @@ -128,6 +130,7 @@ "lint-staged": "^15.4.3", "prettier": "^3.5.0", "prettier-plugin-tailwindcss": "^0.6.11", + "turbo": "^2.8.7", "typescript-eslint": "^8.24.0" }, "overrides": { diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000000..dbbca31ddb --- /dev/null +++ b/turbo.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["package-lock.json"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": [ + "src/**", + "!src/**/__tests__/**", + "!src/**/__mocks__/**", + "!src/**/*.test.*", + "!src/**/*.spec.*", + "scripts/**", + "rollup.config.js", + "server-rollup.config.js", + "tsconfig.json", + "tsconfig.build.json", + "vite.config.ts", + "index.html", + "postcss.config.*", + "tailwind.config.*", + "package.json" + ], + "outputs": ["dist/**"] + }, + "@librechat/data-schemas#build": { + "dependsOn": ["^build", "librechat-data-provider#build"] + }, + "@librechat/api#build": { + "dependsOn": ["^build", "librechat-data-provider#build", "@librechat/data-schemas#build"] + } + } +}