@lenne.tech/cli
Version:
lenne.Tech CLI: lt
369 lines (368 loc) • 18.7 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path");
const framework_detection_1 = require("../../lib/framework-detection");
const frontend_framework_detection_1 = require("../../lib/frontend-framework-detection");
/**
* Convert both backend and frontend of a fullstack monorepo between
* npm mode and vendor mode in a single command.
*
* Usage:
* lt fullstack convert-mode --to vendor [--framework-upstream-branch 11.24.3] [--frontend-framework-upstream-branch 1.5.3]
* lt fullstack convert-mode --to npm
* lt fullstack convert-mode --to vendor --skip-frontend
* lt fullstack convert-mode --to vendor --skip-backend
* lt fullstack convert-mode --to vendor --dry-run
*
* Orchestrates `lt server convert-mode` + `lt frontend convert-mode` with
* auto-detection of `projects/api/` and `projects/app/` subdirectories.
*/
const ConvertModeCommand = {
description: 'Convert fullstack monorepo between npm and vendor modes',
hidden: false,
name: 'convert-mode',
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
const { filesystem, frontendHelper, parameters, print: { colors, error, info, spin, success, warning }, prompt: { confirm }, server, } = toolbox;
// Handle --help-json flag
if (toolbox.tools.helpJson({
description: 'Convert fullstack monorepo (backend + frontend) between npm and vendor modes',
name: 'convert-mode',
options: [
{
description: 'Target mode',
flag: '--to',
required: true,
type: 'string',
values: ['vendor', 'npm'],
},
{
description: 'Backend upstream branch/tag (only with --to vendor, e.g. "11.24.3")',
flag: '--framework-upstream-branch',
required: false,
type: 'string',
},
{
description: 'Frontend upstream branch/tag (only with --to vendor, e.g. "1.5.3")',
flag: '--frontend-framework-upstream-branch',
required: false,
type: 'string',
},
{
description: 'Backend version to install (only with --to npm, default: from VENDOR.md baseline)',
flag: '--framework-version',
required: false,
type: 'string',
},
{
description: 'Frontend version to install (only with --to npm, default: from VENDOR.md baseline)',
flag: '--frontend-framework-version',
required: false,
type: 'string',
},
{
default: false,
description: 'Skip backend conversion',
flag: '--skip-backend',
required: false,
type: 'boolean',
},
{
default: false,
description: 'Skip frontend conversion',
flag: '--skip-frontend',
required: false,
type: 'boolean',
},
{
default: false,
description: 'Show the resolved plan without making any changes',
flag: '--dry-run',
required: false,
type: 'boolean',
},
{
default: false,
description: 'Skip confirmation prompt',
flag: '--noConfirm',
required: false,
type: 'boolean',
},
],
})) {
return;
}
const targetMode = parameters.options.to;
if (!targetMode || !['npm', 'vendor'].includes(targetMode)) {
error('Missing or invalid --to flag. Use: --to vendor or --to npm');
return;
}
const skipBackend = parameters.options['skip-backend'] === true || parameters.options['skip-backend'] === 'true';
const skipFrontend = parameters.options['skip-frontend'] === true || parameters.options['skip-frontend'] === 'true';
const dryRun = parameters.options['dry-run'] === true || parameters.options['dry-run'] === 'true';
const noConfirm = parameters.options.noConfirm === true || parameters.options.noConfirm === 'true';
if (skipBackend && skipFrontend) {
error('Cannot skip both backend and frontend — nothing would happen.');
return;
}
// Locate the subprojects. Typical monorepo layouts: projects/{api,app} or packages/{api,app}.
const cwd = filesystem.cwd();
const backendCandidates = [(0, path_1.join)(cwd, 'projects', 'api'), (0, path_1.join)(cwd, 'packages', 'api')];
const frontendCandidates = [(0, path_1.join)(cwd, 'projects', 'app'), (0, path_1.join)(cwd, 'packages', 'app')];
let backendDir;
for (const candidate of backendCandidates) {
if (filesystem.exists((0, path_1.join)(candidate, 'package.json'))) {
backendDir = candidate;
break;
}
}
let frontendDir;
for (const candidate of frontendCandidates) {
if (filesystem.exists((0, path_1.join)(candidate, 'package.json'))) {
frontendDir = candidate;
break;
}
}
if (!backendDir && !frontendDir) {
error('Could not find any api or app subproject. Expected projects/api, projects/app, packages/api, or packages/app.');
return;
}
// Detect current modes
const backendCurrentMode = backendDir ? (0, framework_detection_1.detectFrameworkMode)(backendDir) : null;
const frontendCurrentMode = frontendDir ? (0, frontend_framework_detection_1.detectFrontendFrameworkMode)(frontendDir) : null;
// Decide what will actually happen
const willConvertBackend = !skipBackend && backendDir && backendCurrentMode && backendCurrentMode !== targetMode;
const willConvertFrontend = !skipFrontend && frontendDir && frontendCurrentMode && frontendCurrentMode !== targetMode;
// ── Plan output ─────────────────────────────────────────────────────
info('');
info(colors.bold('Fullstack convert-mode plan:'));
info(colors.dim('─'.repeat(60)));
if (backendDir) {
const backendLabel = skipBackend
? colors.yellow('(skipped via --skip-backend)')
: backendCurrentMode === targetMode
? colors.dim(`(already in ${targetMode} mode — nothing to do)`)
: colors.green(`${backendCurrentMode} → ${targetMode}`);
info(` Backend: ${backendDir}`);
info(` ${backendLabel}`);
}
else {
info(` Backend: ${colors.dim('(not found)')}`);
}
if (frontendDir) {
const frontendLabel = skipFrontend
? colors.yellow('(skipped via --skip-frontend)')
: frontendCurrentMode === targetMode
? colors.dim(`(already in ${targetMode} mode — nothing to do)`)
: colors.green(`${frontendCurrentMode} → ${targetMode}`);
info(` Frontend: ${frontendDir}`);
info(` ${frontendLabel}`);
}
else {
info(` Frontend: ${colors.dim('(not found)')}`);
}
info(colors.dim('─'.repeat(60)));
if (!willConvertBackend && !willConvertFrontend) {
info('');
warning('Nothing to do: both subprojects are already in the target mode (or skipped).');
return;
}
if (dryRun) {
info('');
info(colors.bold('Would execute:'));
if (willConvertBackend) {
if (targetMode === 'vendor') {
const branch = parameters.options['framework-upstream-branch'] || '(auto-detect from package.json)';
info(` [Backend] lt server convert-mode --to vendor --upstream-branch ${branch} --noConfirm`);
}
else {
const version = parameters.options['framework-version'] || '(from VENDOR.md baseline)';
info(` [Backend] lt server convert-mode --to npm --version ${version} --noConfirm`);
}
}
if (willConvertFrontend) {
if (targetMode === 'vendor') {
const branch = parameters.options['frontend-framework-upstream-branch'] || '(auto-detect from package.json)';
info(` [Frontend] lt frontend convert-mode --to vendor --upstream-branch ${branch} --noConfirm`);
}
else {
const version = parameters.options['frontend-framework-version'] || '(from VENDOR.md baseline)';
info(` [Frontend] lt frontend convert-mode --to npm --version ${version} --noConfirm`);
}
}
info('');
return `fullstack convert-mode dry-run (target: ${targetMode})`;
}
// ── Confirmation ────────────────────────────────────────────────────
if (!noConfirm) {
info('');
const proceed = yield confirm(`Convert ${[willConvertBackend && 'backend', willConvertFrontend && 'frontend'].filter(Boolean).join(' + ')} to ${targetMode} mode?`);
if (!proceed) {
info('Aborted.');
return;
}
}
// ── Execute ─────────────────────────────────────────────────────────
const results = [];
// 1. Backend conversion
if (willConvertBackend && backendDir) {
info('');
info(colors.bold(`[1/2] Backend: ${backendCurrentMode} → ${targetMode}`));
if (targetMode === 'vendor') {
let branch = parameters.options['framework-upstream-branch'];
// Auto-detect version from package.json if not provided
if (!branch) {
try {
const pkg = filesystem.read(`${backendDir}/package.json`, 'json');
const deps = Object.assign(Object.assign({}, ((pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) || {})), ((pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies) || {}));
const version = deps['@lenne.tech/nest-server'];
if (version) {
branch = version.replace(/^[^0-9]*/, '');
info(` Auto-detected @lenne.tech/nest-server version: ${branch}`);
}
}
catch (_a) {
// Will use HEAD
}
}
const spinner = spin(' Converting backend to vendor mode...');
try {
yield server.convertToVendorMode({
dest: backendDir,
upstreamBranch: branch,
});
spinner.succeed(' Backend converted to vendor mode');
results.push({ part: 'backend', status: 'ok' });
}
catch (err) {
spinner.fail(` Backend conversion failed: ${err.message}`);
results.push({ message: err.message, part: 'backend', status: 'failed' });
}
}
else {
const targetVersion = parameters.options['framework-version'];
const spinner = spin(' Converting backend to npm mode...');
try {
yield server.convertToNpmMode({
dest: backendDir,
targetVersion,
});
spinner.succeed(' Backend converted to npm mode');
results.push({ part: 'backend', status: 'ok' });
}
catch (err) {
spinner.fail(` Backend conversion failed: ${err.message}`);
results.push({ message: err.message, part: 'backend', status: 'failed' });
}
}
}
else if (backendDir) {
const reason = skipBackend ? 'skipped via flag' : `already in ${targetMode} mode`;
info('');
info(colors.dim(`[1/2] Backend: ${reason}`));
results.push({ message: reason, part: 'backend', status: 'skipped' });
}
// 2. Frontend conversion
if (willConvertFrontend && frontendDir) {
info('');
info(colors.bold(`[2/2] Frontend: ${frontendCurrentMode} → ${targetMode}`));
if (targetMode === 'vendor') {
let branch = parameters.options['frontend-framework-upstream-branch'];
if (!branch) {
try {
const pkg = filesystem.read(`${frontendDir}/package.json`, 'json');
const deps = Object.assign(Object.assign({}, ((pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) || {})), ((pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies) || {}));
const version = deps['@lenne.tech/nuxt-extensions'];
if (version) {
branch = version.replace(/^[^0-9]*/, '');
info(` Auto-detected @lenne.tech/nuxt-extensions version: ${branch}`);
}
}
catch (_b) {
// Will use HEAD
}
}
const spinner = spin(' Converting frontend to vendor mode...');
try {
yield frontendHelper.convertAppToVendorMode({
dest: frontendDir,
upstreamBranch: branch,
});
spinner.succeed(' Frontend converted to vendor mode');
results.push({ part: 'frontend', status: 'ok' });
}
catch (err) {
spinner.fail(` Frontend conversion failed: ${err.message}`);
results.push({ message: err.message, part: 'frontend', status: 'failed' });
}
}
else {
const targetVersion = parameters.options['frontend-framework-version'];
const spinner = spin(' Converting frontend to npm mode...');
try {
yield frontendHelper.convertAppToNpmMode({
dest: frontendDir,
targetVersion,
});
spinner.succeed(' Frontend converted to npm mode');
results.push({ part: 'frontend', status: 'ok' });
}
catch (err) {
spinner.fail(` Frontend conversion failed: ${err.message}`);
results.push({ message: err.message, part: 'frontend', status: 'failed' });
}
}
}
else if (frontendDir) {
const reason = skipFrontend ? 'skipped via flag' : `already in ${targetMode} mode`;
info('');
info(colors.dim(`[2/2] Frontend: ${reason}`));
results.push({ message: reason, part: 'frontend', status: 'skipped' });
}
// ── Summary ─────────────────────────────────────────────────────────
info('');
info(colors.bold('Summary:'));
info(colors.dim('─'.repeat(60)));
for (const result of results) {
const icon = result.status === 'ok' ? colors.green('✓') : result.status === 'skipped' ? colors.dim('–') : colors.red('✗');
const label = result.part.padEnd(10);
info(` ${icon} ${label} ${result.message || result.status}`);
}
info(colors.dim('─'.repeat(60)));
const failed = results.filter((r) => r.status === 'failed');
if (failed.length > 0) {
info('');
error(`${failed.length} conversion(s) failed. See messages above.`);
if (!parameters.options.fromGluegunMenu) {
process.exit(1);
}
return `fullstack convert-mode failed (${failed.length} error(s))`;
}
info('');
success('All conversions completed successfully.');
info('');
info(colors.bold('Next steps:'));
info(' 1. Run: pnpm install (from monorepo root)');
if (backendDir && results.find((r) => r.part === 'backend' && r.status === 'ok')) {
info(' 2. Verify backend: cd projects/api && pnpm exec tsc --noEmit && pnpm test');
}
if (frontendDir && results.find((r) => r.part === 'frontend' && r.status === 'ok')) {
info(' 3. Verify frontend: cd projects/app && pnpm run build');
}
info(' 4. Commit the changes');
info('');
if (!parameters.options.fromGluegunMenu) {
process.exit();
}
return `fullstack convert-mode completed (target: ${targetMode})`;
}),
};
exports.default = ConvertModeCommand;