UNPKG

@rushstack/lockfile-explorer

Version:

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

204 lines 9.38 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 open from 'open'; import updateNotifier from 'update-notifier'; import { FileSystem, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library'; import { ConsoleTerminalProvider, Terminal, 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'; const EXPLORER_TOOL_FILENAME = 'lockfile-explorer'; export class ExplorerCommandLineParser extends CommandLineParser { constructor() { 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._terminalProvider = new ConsoleTerminalProvider(); this.globalTerminal = new Terminal(this._terminalProvider); } get isDebug() { return this._debugParameter.value; } async onExecuteAsync() { const lockfileExplorerProjectRoot = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname); const lockfileExplorerPackageJson = JsonFile.load(`${lockfileExplorerProjectRoot}/package.json`); const appVersion = lockfileExplorerPackageJson.version; this.globalTerminal.writeLine(Colorize.bold(`\nRush Lockfile Explorer ${appVersion}`) + Colorize.cyan(' - https://lfx.rushstack.io/\n')); updateNotifier({ pkg: lockfileExplorerPackageJson, // Normally update-notifier waits a day or so before it starts displaying upgrade notices. // In debug mode, show the notice right away. updateCheckInterval: this.isDebug ? 0 : undefined }).notify({ // Make sure it says "-g" in the "npm install" example command line isGlobal: true, // Show the notice immediately, rather than waiting for process.onExit() defer: false }); const PORT = 8091; // Must not have a trailing slash const SERVICE_URL = `http://localhost:${PORT}`; const appState = init({ lockfileExplorerProjectRoot, appVersion, 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) { console.log(Colorize.red('The client has disconnected!')); console.log(`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; console.log(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 () => { console.log(`App launched on ${SERVICE_URL}`); if (!appState.debugMode) { try { // Launch the web browser await open(SERVICE_URL); } catch (e) { this.globalTerminal.writeError('Error launching browser: ' + e.toString()); } } }); } } //# sourceMappingURL=ExplorerCommandLineParser.js.map