UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

114 lines (113 loc) 4.18 kB
import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, relative } from 'node:path'; import { parse as parseYaml } from 'yaml'; import { logWarn } from './log.js'; const GATED_PREFIXES = ['@mesh-tech/']; export const PROGRAM_EVAL_OPS = new Set(['up', 'preview', 'destroy', 'refresh', 'import']); export const SKIP_ENV = 'MESH_SKIP_DEPLOY_PREFLIGHT'; function isGated(name) { return GATED_PREFIXES.some((p) => name.startsWith(p)); } export function normalizeLockVersion(raw) { if (typeof raw !== 'string' || raw.length === 0) return null; const bare = raw.split('(')[0].trim(); return /^\d/.test(bare) ? bare : null; } export function extractImporterMeshVersions(lockDoc, importerRel) { const out = {}; const importers = lockDoc?.importers; const importer = importers?.[importerRel]; if (!importer) return out; for (const group of ['dependencies', 'devDependencies', 'optionalDependencies']) { const deps = importer[group]; if (!deps) continue; for (const [name, entry] of Object.entries(deps)) { if (!isGated(name)) continue; const raw = typeof entry === 'string' ? entry : entry?.version; const version = normalizeLockVersion(raw); if (version) out[name] = version; } } return out; } export function diffLockVsInstalled(expected, installed) { const out = []; for (const [name, exp] of Object.entries(expected)) { const got = installed[name] ?? null; if (got !== exp) out.push({ name, expected: exp, installed: got }); } return out; } export function findPnpmLock(startDir) { let dir = startDir; for (;;) { const candidate = join(dir, 'pnpm-lock.yaml'); if (existsSync(candidate)) return candidate; const parent = dirname(dir); if (parent === dir) return null; dir = parent; } } function readInstalledVersion(fromDir, name) { try { const pkgPath = join(fromDir, 'node_modules', name, 'package.json'); const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version; return typeof version === 'string' ? version : null; } catch { return null; } } export function checkDeployDepsFresh(appRoot) { const lockPath = findPnpmLock(appRoot); if (!lockPath) return null; let lockDoc; try { lockDoc = parseYaml(readFileSync(lockPath, 'utf8')); } catch { logWarn(`deploy preflight: could not parse ${lockPath} — skipping the stale-node_modules check.`); return null; } const importerRel = relative(dirname(lockPath), appRoot) || '.'; const expected = extractImporterMeshVersions(lockDoc, importerRel); const names = Object.keys(expected); if (names.length === 0) return []; const installed = {}; for (const name of names) installed[name] = readInstalledVersion(appRoot, name); return diffLockVsInstalled(expected, installed); } export function formatStaleDepsError(mismatches) { const lines = [ `✗ ${mismatches.length} @mesh-tech/* dep${mismatches.length === 1 ? '' : 's'} in node_modules ` + `do not match the lockfile — run pnpm install before deploying:`, '', ]; for (const m of mismatches) { lines.push(` • ${m.name} — installed ${m.installed ?? '(missing)'}, lockfile wants ${m.expected}`); } lines.push('', ' A stale node_modules makes pulumi evaluate OLD component code and can plan to', ' DELETE resources the current code would keep. Fix:', '', ' pnpm install', '', ` To bypass this check (rarely correct): ${SKIP_ENV}=1 mesh deploy …`); return lines.join('\n'); } export function assertDeployDepsFresh(appRoot, op) { if (!PROGRAM_EVAL_OPS.has(op)) return; if (process.env[SKIP_ENV]) return; const mismatches = checkDeployDepsFresh(appRoot); if (!mismatches || mismatches.length === 0) return; process.stderr.write(formatStaleDepsError(mismatches) + '\n'); process.exit(1); }