UNPKG

@rushstack/lockfile-explorer

Version:

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

205 lines 9.7 kB
"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExplorerCommandLineParser = void 0; const express_1 = __importDefault(require("express")); const js_yaml_1 = __importDefault(require("js-yaml")); const cors_1 = __importDefault(require("cors")); const process_1 = __importDefault(require("process")); const open_1 = __importDefault(require("open")); const update_notifier_1 = __importDefault(require("update-notifier")); const node_core_library_1 = require("@rushstack/node-core-library"); const terminal_1 = require("@rushstack/terminal"); const ts_command_line_1 = require("@rushstack/ts-command-line"); const init_1 = require("../../utils/init"); const shrinkwrap_1 = require("../../utils/shrinkwrap"); const EXPLORER_TOOL_FILENAME = 'lockfile-explorer'; class ExplorerCommandLineParser extends ts_command_line_1.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 terminal_1.ConsoleTerminalProvider(); this.globalTerminal = new terminal_1.Terminal(this._terminalProvider); } get isDebug() { return this._debugParameter.value; } async onExecuteAsync() { const lockfileExplorerProjectRoot = node_core_library_1.PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname); const lockfileExplorerPackageJson = node_core_library_1.JsonFile.load(`${lockfileExplorerProjectRoot}/package.json`); const appVersion = lockfileExplorerPackageJson.version; this.globalTerminal.writeLine(terminal_1.Colorize.bold(`\nRush Lockfile Explorer ${appVersion}`) + terminal_1.Colorize.cyan(' - https://lfx.rushstack.io/\n')); (0, update_notifier_1.default)({ 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 = (0, init_1.init)({ lockfileExplorerProjectRoot, appVersion, debugMode: this.isDebug, subspaceName: this._subspaceParameter.value }); // Important: This must happen after init() reads the current working directory process_1.default.chdir(appState.lockfileExplorerProjectRoot); const distFolderPath = `${appState.lockfileExplorerProjectRoot}/dist`; const app = (0, express_1.default)(); app.use(express_1.default.json()); app.use((0, cors_1.default)()); // 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(terminal_1.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_1.default.static(distFolderPath)); app.use('/favicon.ico', express_1.default.static(distFolderPath, { index: 'favicon.ico' })); app.get('/api/lockfile', async (req, res) => { const pnpmLockfileText = await node_core_library_1.FileSystem.readFileAsync(appState.pnpmLockfileLocation); const doc = js_yaml_1.default.load(pnpmLockfileText); const { packages, lockfileVersion } = doc; const shrinkwrapFileMajorVersion = (0, shrinkwrap_1.getShrinkwrapFileMajorVersion)(lockfileVersion); if (packages && shrinkwrapFileMajorVersion === 6) { const updatedPackages = {}; for (const [dependencyPath, dependency] of Object.entries(packages)) { updatedPackages[(0, shrinkwrap_1.convertLockfileV6DepPathToV5DepPath)(dependencyPath)] = dependency; } doc.packages = updatedPackages; } res.send({ doc, subspaceName: this._subspaceParameter.value }); }); app.get('/api/health', (req, res) => { awaitingFirstConnect = false; isClientConnected = true; if (disconnected) { disconnected = false; console.log(terminal_1.Colorize.green('The client has reconnected!')); } res.status(200).send(); }); app.post('/api/package-json', async (req, res) => { const { projectPath } = req.body; const fileLocation = `${appState.projectRoot}/${projectPath}/package.json`; let packageJsonText; try { packageJsonText = await node_core_library_1.FileSystem.readFileAsync(fileLocation); } catch (e) { if (node_core_library_1.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) => { let pnpmfile; try { pnpmfile = await node_core_library_1.FileSystem.readFileAsync(appState.pnpmfileLocation); } catch (e) { if (node_core_library_1.FileSystem.isNotExistError(e)) { return res.status(404).send({ message: `Could not load pnpmfile file in this repo.`, 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 node_core_library_1.JsonFile.loadAsync(fileLocation); } catch (e) { if (node_core_library_1.FileSystem.isNotExistError(e)) { return res.status(404).send({ message: `Could not load package.json file in location: ${projectPath}` }); } else { throw e; } } const { hooks: { readPackage } } = require(appState.pnpmfileLocation); const parsedPackage = readPackage(packageJson, {}); res.send(parsedPackage); }); app.listen(PORT, async () => { console.log(`App launched on ${SERVICE_URL}`); if (!appState.debugMode) { try { // Launch the web browser await (0, open_1.default)(SERVICE_URL); } catch (e) { this.globalTerminal.writeError('Error launching browser: ' + e.toString()); } } }); } } exports.ExplorerCommandLineParser = ExplorerCommandLineParser; //# sourceMappingURL=ExplorerCommandLineParser.js.map