UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

285 lines (284 loc) 15.4 kB
"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 hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config"); const workspace_integration_1 = require("../../lib/workspace-integration"); /** * Add a frontend app (`projects/app/`) to a fullstack workspace that * currently only ships an API (`projects/api/`). Mirrors every * frontend-related flag from `lt fullstack init` so the surface area * stays in lockstep. * * Refuses to run if `projects/app/` already exists. */ const NewCommand = { alias: ['add-app'], description: 'Add app to fullstack workspace', hidden: false, name: 'add-app', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; const { config, filesystem, frontendHelper, git, parameters, print: { error, info, spin, success, warning }, prompt: { ask }, system, } = toolbox; if (toolbox.tools.helpJson({ aliases: ['add-app'], configuration: 'commands.fullstack.*', description: 'Add a frontend app to an existing fullstack workspace', name: 'add-app', options: [ { description: 'Frontend framework', flag: '--frontend', type: 'string', values: ['nuxt', 'angular'] }, { description: 'Frontend framework consumption mode', flag: '--frontend-framework-mode', type: 'string', values: ['npm', 'vendor'], }, { description: 'Branch of the frontend starter to clone', flag: '--frontend-branch', type: 'string' }, { description: 'Path to local frontend template to copy from', flag: '--frontend-copy', type: 'string' }, { description: 'Path to local frontend template to symlink', flag: '--frontend-link', type: 'string' }, { description: 'Use experimental nuxt-base-starter `next` branch', flag: '--next', type: 'boolean' }, { description: 'Workspace root (defaults to cwd)', flag: '--workspace-dir', type: 'string' }, { description: 'Skip install / format after app integration', flag: '--skip-install', type: 'boolean' }, { description: 'Print resolved plan and exit without disk changes', flag: '--dry-run', type: 'boolean' }, { description: 'Skip all interactive prompts', flag: '--noConfirm', type: 'boolean' }, ], })) { return; } const timer = system.startTimer(); info('Add app to fullstack workspace'); toolbox.tools.nonInteractiveHint('lt fullstack add-app --frontend <nuxt|angular> [--frontend-branch <ref>] [--next] [--dry-run] --noConfirm'); if (!(yield git.gitInstalled())) { return; } const ltConfig = config.loadConfig(); const cliFrontend = parameters.options.frontend; const cliFrontendFrameworkMode = parameters.options['frontend-framework-mode']; const cliFrontendBranch = parameters.options['frontend-branch']; const cliFrontendCopy = parameters.options['frontend-copy']; const cliFrontendLink = parameters.options['frontend-link']; const cliWorkspaceDir = parameters.options['workspace-dir']; const cliDryRun = parameters.options['dry-run']; const cliSkipInstall = parameters.options['skip-install']; const experimental = parameters.options.next === true || parameters.options.next === 'true'; const dryRun = cliDryRun === true || cliDryRun === 'true'; const skipInstall = cliSkipInstall === true || cliSkipInstall === 'true'; const noConfirm = config.getNoConfirm({ cliValue: parameters.options.noConfirm, commandConfig: (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.fullstack, config: ltConfig, }); const configFrontend = (_c = (_b = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _b === void 0 ? void 0 : _b.fullstack) === null || _c === void 0 ? void 0 : _c.frontend; const configFrontendFrameworkMode = (_e = (_d = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _d === void 0 ? void 0 : _d.fullstack) === null || _e === void 0 ? void 0 : _e.frontendFrameworkMode; const configFrontendBranch = (_g = (_f = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _f === void 0 ? void 0 : _f.fullstack) === null || _g === void 0 ? void 0 : _g.frontendBranch; const configFrontendCopy = (_j = (_h = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _h === void 0 ? void 0 : _h.fullstack) === null || _j === void 0 ? void 0 : _j.frontendCopy; const configFrontendLink = (_l = (_k = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _k === void 0 ? void 0 : _k.fullstack) === null || _l === void 0 ? void 0 : _l.frontendLink; // Workspace detection — same priority as `add-api`: // 1. explicit `--workspace-dir <path>` always wins // 2. cwd if it itself is a workspace // 3. nearest workspace by walking up from cwd (so users running // from inside `projects/api/src/` don't have to pass // `--workspace-dir ../..`) let workspaceDir; if (cliWorkspaceDir) { workspaceDir = cliWorkspaceDir; } else { const cwdLayout = (0, workspace_integration_1.detectWorkspaceLayout)('.', filesystem); if (cwdLayout.hasWorkspace) { workspaceDir = '.'; } else { const upRoot = (0, workspace_integration_1.findWorkspaceRoot)('.', filesystem); if (upRoot) { workspaceDir = upRoot; info(`Detected fullstack workspace at ${upRoot} (walked up from cwd).`); } else { workspaceDir = '.'; } } } const layout = (0, workspace_integration_1.detectWorkspaceLayout)(workspaceDir, filesystem); if (!layout.hasWorkspace) { error(`No fullstack workspace detected at "${workspaceDir}". Expected pnpm-workspace.yaml, package.json#workspaces, or a projects/ directory. Use \`lt fullstack init\` for a fresh workspace.`); return; } if (layout.hasApp) { error(`An app already exists at "${workspaceDir}/projects/app". Remove it first or use \`lt fullstack init\` in a fresh directory.`); return; } // Resolve frontend. let frontend; if (cliFrontend === 'angular' || cliFrontend === 'nuxt') { frontend = cliFrontend; } else if (cliFrontend) { error('Invalid --frontend option. Use "angular" or "nuxt".'); return; } else if (configFrontend === 'angular' || configFrontend === 'nuxt') { frontend = configFrontend; info(`Using frontend from lt.config: ${frontend}`); } else if (noConfirm) { frontend = 'nuxt'; info('Using default frontend: nuxt (noConfirm mode)'); } else { const choice = yield ask({ choices: ['angular', 'nuxt'], initial: 1, message: 'Which frontend framework?', name: 'frontend', type: 'select', }); frontend = (choice.frontend === 'angular' ? 'angular' : 'nuxt'); } // Resolve frontend framework mode. let frontendFrameworkMode; if (cliFrontendFrameworkMode === 'npm' || cliFrontendFrameworkMode === 'vendor') { frontendFrameworkMode = cliFrontendFrameworkMode; } else if (cliFrontendFrameworkMode) { error(`Invalid --frontend-framework-mode value "${cliFrontendFrameworkMode}". Use "npm" or "vendor".`); return; } else if (configFrontendFrameworkMode === 'npm' || configFrontendFrameworkMode === 'vendor') { frontendFrameworkMode = configFrontendFrameworkMode; info(`Using frontend framework mode from lt.config: ${frontendFrameworkMode}`); } else { frontendFrameworkMode = 'npm'; } // Branch / copy / link with the same `--next` default that init.ts // applies: under `--next`, default the nuxt-base-starter ref to the // `next` branch (auth basePath aligned with the experimental API). const frontendBranch = cliFrontendBranch || configFrontendBranch || (experimental && frontend === 'nuxt' ? 'next' : undefined); const frontendCopy = cliFrontendCopy || configFrontendCopy; const frontendLink = cliFrontendLink || configFrontendLink; // Derive a project name (kebab-case workspace slug) for env patching. let projectName = parameters.options.name; if (!projectName) { const apiPkgPath = filesystem.path(workspaceDir, 'projects', 'api', 'package.json'); if (filesystem.exists(apiPkgPath)) { const apiPkg = filesystem.read(apiPkgPath, 'json'); if (apiPkg && typeof apiPkg.name === 'string' && apiPkg.name) { projectName = apiPkg.name; } } } if (!projectName) { const segments = filesystem.path(workspaceDir).split(/[\\/]/).filter(Boolean); projectName = segments[segments.length - 1] || 'fullstack-app'; } if (dryRun) { info(''); info('Dry-run plan:'); info(` workspaceDir: ${workspaceDir}`); info(` projectName: ${projectName}`); info(` frontend: ${frontend}`); info(` frontendFrameworkMode: ${frontendFrameworkMode}`); info(` frontendBranch: ${frontendBranch || '(default)'}`); info(` frontendCopy: ${frontendCopy || '(none)'}`); info(` frontendLink: ${frontendLink || '(none)'}`); info(` experimental (--next): ${experimental}`); info(''); info('Would execute:'); info(` 1. setup ${frontend} → ${workspaceDir}/projects/app`); if (frontend === 'nuxt' && frontendFrameworkMode === 'vendor') { info(` 2. clone @lenne.tech/nuxt-extensions → /tmp`); info(` 3. vendor app/core/ (module.ts + runtime/)`); info(` 4. rewrite nuxt.config.ts module entry`); } info(` N. patch projects/app/.env with NUXT_PUBLIC_STORAGE_PREFIX`); if (!skipInstall) info(` M. pnpm install + format projects/app`); info(''); return `fullstack add-app dry-run (${frontend} / ${frontendFrameworkMode})`; } const appDest = `${workspaceDir}/projects/app`; const appSpinner = spin(`Integrate ${frontend}${frontendLink ? ' (link)' : frontendCopy ? ' (copy)' : frontendBranch ? ` (branch: ${frontendBranch})` : ''}`); const isNuxt = frontend === 'nuxt'; const result = isNuxt ? yield frontendHelper.setupNuxt(appDest, { branch: frontendBranch, copyPath: frontendCopy, linkPath: frontendLink, skipInstall: true, }) : yield frontendHelper.setupAngular(appDest, { branch: frontendBranch, copyPath: frontendCopy, linkPath: frontendLink, skipGitInit: true, skipHuskyRemoval: true, skipInstall: true, }); if (!result.success) { appSpinner.fail(`Failed to set up ${frontend} frontend: ${result.path}`); return; } appSpinner.succeed(`${frontend} integrated (${result.method})`); // Patch frontend .env (skip on link mode — points at user's checkout). if (result.method !== 'link') { frontendHelper.patchFrontendEnv(appDest, projectName); } // Vendor frontend if requested. Skipped on link mode for the same // reason as `init.ts`. if (isNuxt && frontendFrameworkMode === 'vendor' && result.method !== 'link') { const vendorSpinner = spin('Converting frontend to vendor mode...'); try { yield frontendHelper.convertAppCloneToVendored({ dest: appDest, projectName, }); vendorSpinner.succeed('Frontend converted to vendor mode (app/core/)'); } catch (err) { vendorSpinner.fail(`Frontend vendor conversion failed: ${err.message}`); warning('Continuing with npm mode for frontend.'); } } // Hoist pnpm config (frontend templates may carry pnpm.overrides too). (0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({ filesystem, projectDir: workspaceDir, subProjects: ['projects/api', 'projects/app'], }); if (!skipInstall) { const installSpinner = spin('Install workspace packages'); try { const detectedPm = toolbox.pm.detect(workspaceDir); yield system.run(`cd ${workspaceDir} && ${toolbox.pm.install(detectedPm)}`); installSpinner.succeed('Successfully installed workspace packages'); } catch (err) { installSpinner.fail(`Failed to install packages: ${err.message}`); warning('Run install manually after fixing the issue.'); } if (isNuxt && filesystem.isDirectory(appDest)) { yield toolbox.apiMode.formatProject(appDest); } } info(''); success(`App integrated into ${workspaceDir} in ${toolbox.helper.msToMinutesAndSeconds(timer())}m.`); info(''); info('Next:'); info(` $ cd ${workspaceDir}`); info(` $ ${toolbox.pm.run('start')}`); info(''); if (!toolbox.parameters.options.fromGluegunMenu) { process.exit(); } return `added app to workspace ${workspaceDir}`; }), }; exports.default = NewCommand;