@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
114 lines (113 loc) • 4.18 kB
JavaScript
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);
}