UNPKG

@rushstack/lockfile-explorer

Version:

Rush Lockfile Explorer: The UI for solving version conflicts quickly in a large monorepo

230 lines 10.8 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import process from 'node:process'; import * as path from 'node:path'; import express from 'express'; import yaml from 'js-yaml'; import cors from 'cors'; import { Executable, FileSystem, JsonFile } from '@rushstack/node-core-library'; import { Colorize } from '@rushstack/terminal'; import { CommandLineParser } from '@rushstack/ts-command-line'; import { lfxGraphSerializer } from '../../../build/lfx-shared'; import * as lockfilePath from '../../graph/lockfilePath'; import { init } from '../../utils/init'; import { PnpmfileRunner } from '../../graph/PnpmfileRunner'; import * as lfxGraphLoader from '../../graph/lfxGraphLoader'; import { LFX_PACKAGE_NAME, LFX_VERSION } from '../../utils/constants'; import { PackageUpdateChecker } from '../../utils/PackageUpdateChecker'; const EXPLORER_TOOL_FILENAME = 'lockfile-explorer'; function printUpdateNotification(result, terminal) { if (result === null || result === void 0 ? void 0 : result.isOutdated) { terminal.writeLine(Colorize.yellow(`\nUpdate available: ${LFX_VERSION}${result.latestVersion}\n` + `Run: npm install -g ${LFX_PACKAGE_NAME}\n`)); } } export class ExplorerCommandLineParser extends CommandLineParser { constructor(terminal) { super({ toolFilename: EXPLORER_TOOL_FILENAME, toolDescription: 'Lockfile Explorer is a desktop app for investigating and solving version conflicts in a PNPM workspace.' }); this._debugParameter = this.defineFlagParameter({ parameterLongName: '--debug', parameterShortName: '-d', description: 'Show the full call stack if an error occurs while executing the tool' }); this._subspaceParameter = this.defineStringParameter({ parameterLongName: '--subspace', argumentName: 'SUBSPACE_NAME', description: 'Specifies an individual Rush subspace to check.', defaultValue: 'default' }); this.globalTerminal = terminal; } get isDebug() { return this._debugParameter.value; } async onExecuteAsync() { const terminal = this.globalTerminal; terminal.writeLine(Colorize.bold(`\nRush Lockfile Explorer ${LFX_VERSION}`) + Colorize.cyan(' - https://lfx.rushstack.io/\n')); // Start the update check now so it runs concurrently with server setup. // The result is awaited and displayed inside app.listen once the server is ready. const updateChecker = new PackageUpdateChecker({ packageName: LFX_PACKAGE_NAME, currentVersion: LFX_VERSION, // In debug mode, bypass the cache so the notice appears immediately. forceCheck: this.isDebug }); const updateCheckPromise = updateChecker.tryGetUpdateAsync(); const PORT = 8091; // Must not have a trailing slash const SERVICE_URL = `http://localhost:${PORT}`; const appState = init({ appVersion: LFX_VERSION, debugMode: this.isDebug, subspaceName: this._subspaceParameter.value }); const lfxWorkspace = appState.lfxWorkspace; // Important: This must happen after init() reads the current working directory process.chdir(appState.lockfileExplorerProjectRoot); const distFolderPath = `${appState.lockfileExplorerProjectRoot}/dist`; const app = express(); app.use(express.json()); app.use(cors()); // Variable used to check if the front-end client is still connected let awaitingFirstConnect = true; let isClientConnected = false; let disconnected = false; setInterval(() => { if (!isClientConnected && !awaitingFirstConnect && !disconnected) { terminal.writeLine(Colorize.red('The client has disconnected!')); terminal.writeLine(`Please open a browser window at http://localhost:${PORT}/app`); disconnected = true; } else if (!awaitingFirstConnect) { isClientConnected = false; } }, 4000); // This takes precedence over the `/app` static route, which also has an `initappcontext.js` file. app.get('/initappcontext.js', (req, res) => { const appContext = { serviceUrl: SERVICE_URL, appVersion: appState.appVersion, debugMode: this.isDebug }; const sourceCode = [ `console.log('Loaded initappcontext.js');`, `appContext = ${JSON.stringify(appContext)}` ].join('\n'); res.type('application/javascript').send(sourceCode); }); app.use('/', express.static(distFolderPath)); app.use('/favicon.ico', express.static(distFolderPath, { index: 'favicon.ico' })); app.get('/api/health', (req, res) => { awaitingFirstConnect = false; isClientConnected = true; if (disconnected) { disconnected = false; terminal.writeLine(Colorize.green('The client has reconnected!')); } res.status(200).send(); }); app.get('/api/graph', async (req, res) => { const pnpmLockfileText = await FileSystem.readFileAsync(appState.pnpmLockfileLocation); const lockfile = yaml.load(pnpmLockfileText); const graph = lfxGraphLoader.generateLockfileGraph(lockfile, lfxWorkspace); const jsonGraph = lfxGraphSerializer.serializeToJson(graph); res.type('application/json').send(jsonGraph); }); app.post('/api/package-json', async (req, res) => { const { projectPath } = req.body; const fileLocation = `${appState.projectRoot}/${projectPath}/package.json`; let packageJsonText; try { packageJsonText = await FileSystem.readFileAsync(fileLocation); } catch (e) { if (FileSystem.isNotExistError(e)) { return res.status(404).send({ message: `Could not load package.json file for this package. Have you installed all the dependencies for this workspace?`, error: `No package.json in location: ${projectPath}` }); } else { throw e; } } res.send(packageJsonText); }); app.get('/api/pnpmfile', async (req, res) => { var _a, _b; const pnpmfilePath = lockfilePath.join(lfxWorkspace.workspaceRootFullPath, (_b = (_a = lfxWorkspace.rushConfig) === null || _a === void 0 ? void 0 : _a.rushPnpmfilePath) !== null && _b !== void 0 ? _b : lfxWorkspace.pnpmfilePath); let pnpmfile; try { pnpmfile = await FileSystem.readFileAsync(pnpmfilePath); } catch (e) { if (FileSystem.isNotExistError(e)) { return res.status(404).send({ message: `Could not load .pnpmfile.cjs file in this repo: "${pnpmfilePath}"`, error: `No .pnpmifile.cjs found.` }); } else { throw e; } } res.send(pnpmfile); }); app.post('/api/package-spec', async (req, res) => { const { projectPath } = req.body; const fileLocation = `${appState.projectRoot}/${projectPath}/package.json`; let packageJson; try { packageJson = await JsonFile.loadAsync(fileLocation); } catch (e) { if (FileSystem.isNotExistError(e)) { return res.status(404).send({ message: `Could not load package.json file in location: ${projectPath}` }); } else { throw e; } } let parsedPackage = packageJson; const pnpmfilePath = path.join(lfxWorkspace.workspaceRootFullPath, lfxWorkspace.pnpmfilePath); if (await FileSystem.existsAsync(pnpmfilePath)) { const pnpmFileRunner = new PnpmfileRunner(pnpmfilePath); try { parsedPackage = await pnpmFileRunner.transformPackageAsync(packageJson, fileLocation); } finally { await pnpmFileRunner.disposeAsync(); } } res.send(parsedPackage); }); app.listen(PORT, async () => { terminal.writeLine(`App launched on ${SERVICE_URL}`); printUpdateNotification(await updateCheckPromise, terminal); if (!appState.debugMode) { try { // Launch the default web browser using the platform-native open command. let browserCmd; let browserArgs; switch (process.platform) { case 'win32': { // "start" is a cmd.exe built-in, not a standalone executable. // The empty string is the required [title] argument; without it, // cmd interprets the URL as the title and ignores it. browserCmd = 'cmd'; browserArgs = ['/c', 'start', '', SERVICE_URL]; break; } case 'darwin': { browserCmd = 'open'; browserArgs = [SERVICE_URL]; break; } default: { // Linux and other Unix-like systems browserCmd = 'xdg-open'; browserArgs = [SERVICE_URL]; break; } } const browserProcess = Executable.spawn(browserCmd, browserArgs, { stdio: 'ignore' }); // Detach from our Node.js process so the browser stays open after we exit browserProcess.unref(); } catch (e) { terminal.writeError('Error launching browser: ' + e.toString()); } } }); } } //# sourceMappingURL=ExplorerCommandLineParser.js.map