|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. |
| 2 | +// See LICENSE in the project root for license information. |
| 3 | + |
| 4 | +import * as child_process from 'child_process'; |
| 5 | +import * as colors from 'colors'; |
| 6 | +import * as os from 'os'; |
| 7 | +import * as path from 'path'; |
| 8 | + |
| 9 | +import { |
| 10 | + PackageJsonLookup, |
| 11 | + IPackageJson, |
| 12 | + Text, |
| 13 | + IPackageJsonScriptTable |
| 14 | + } from '@microsoft/node-core-library'; |
| 15 | +import { Utilities } from './utilities/Utilities'; |
| 16 | + |
| 17 | +/** |
| 18 | + * Parses the "scripts" section from package.json |
| 19 | + */ |
| 20 | +class ProjectCommandSet { |
| 21 | + public readonly malformedScriptNames: string[] = []; |
| 22 | + public readonly commandNames: string[] = []; |
| 23 | + private readonly _scriptsByName: Map<string, string> = new Map<string, string>(); |
| 24 | + |
| 25 | + public constructor(packageJson: IPackageJson) { |
| 26 | + const scripts: IPackageJsonScriptTable = packageJson.scripts || { }; |
| 27 | + |
| 28 | + for (const scriptName of Object.keys(scripts)) { |
| 29 | + if (scriptName[0] === '-' || scriptName.length === 0) { |
| 30 | + this.malformedScriptNames.push(scriptName); |
| 31 | + } else { |
| 32 | + this.commandNames.push(scriptName); |
| 33 | + this._scriptsByName.set(scriptName, scripts[scriptName]); |
| 34 | + } |
| 35 | + } |
| 36 | + |
| 37 | + this.commandNames.sort(); |
| 38 | + } |
| 39 | + |
| 40 | + public tryGetScriptBody(commandName: string): string | undefined { |
| 41 | + return this._scriptsByName.get(commandName); |
| 42 | + } |
| 43 | + |
| 44 | + public getScriptBody(commandName: string): string { |
| 45 | + const result: string | undefined = this.tryGetScriptBody(commandName); |
| 46 | + if (result === undefined) { |
| 47 | + throw new Error(`The command "${commandName}" was not found`); |
| 48 | + } |
| 49 | + return result; |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +export class RushX { |
| 54 | + public static launchRushX(launcherVersion: string, isManaged: boolean): void { |
| 55 | + // NodeJS can sometimes accidentally terminate with a zero exit code (e.g. for an uncaught |
| 56 | + // promise exception), so we start with the assumption that the exit code is 1 |
| 57 | + // and set it to 0 only on success. |
| 58 | + process.exitCode = 1; |
| 59 | + |
| 60 | + try { |
| 61 | + // Find the governing package.json for this folder: |
| 62 | + const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); |
| 63 | + |
| 64 | + const packageJsonFilePath: string | undefined = packageJsonLookup.tryGetPackageJsonFilePathFor(process.cwd()); |
| 65 | + if (!packageJsonFilePath) { |
| 66 | + console.log(colors.red('This command should be used inside a project folder.')); |
| 67 | + console.log(`Unable to find a package.json file in the current working directory or any of its parents.`); |
| 68 | + return; |
| 69 | + } |
| 70 | + |
| 71 | + const packageJson: IPackageJson = packageJsonLookup.loadPackageJson(packageJsonFilePath); |
| 72 | + |
| 73 | + const projectCommandSet: ProjectCommandSet = new ProjectCommandSet(packageJson); |
| 74 | + |
| 75 | + // 0 = node.exe |
| 76 | + // 1 = rushx |
| 77 | + const args: string[] = process.argv.slice(2); |
| 78 | + |
| 79 | + // Check for the following types of things: |
| 80 | + // rush |
| 81 | + // rush --help |
| 82 | + // rush -h |
| 83 | + // rush --unrecognized-option |
| 84 | + if (args.length === 0 || args[0][0] === '-') { |
| 85 | + RushX._showUsage(packageJson, projectCommandSet); |
| 86 | + return; |
| 87 | + } |
| 88 | + |
| 89 | + const commandName: string = args[0]; |
| 90 | + |
| 91 | + const scriptBody: string | undefined = projectCommandSet.tryGetScriptBody(commandName); |
| 92 | + |
| 93 | + if (scriptBody === undefined) { |
| 94 | + console.log(colors.red(`Error: The command "${commandName}" is not defined in the` |
| 95 | + + ` package.json file for this project.`)); |
| 96 | + |
| 97 | + if (projectCommandSet.commandNames.length > 0) { |
| 98 | + console.log(os.EOL + 'Available commands for this project are: ' |
| 99 | + + projectCommandSet.commandNames.map(x => `"${x}"`).join(', ')); |
| 100 | + } |
| 101 | + |
| 102 | + console.log(`Use ${colors.yellow('"rushx --help"')} for more information.`); |
| 103 | + return; |
| 104 | + } |
| 105 | + |
| 106 | + console.log('Executing: ' + JSON.stringify(scriptBody) + os.EOL); |
| 107 | + |
| 108 | + const packageFolder: string = path.dirname(packageJsonFilePath); |
| 109 | + |
| 110 | + const result: child_process.ChildProcess = Utilities.executeLifecycleCommandAsync( |
| 111 | + scriptBody, |
| 112 | + packageFolder, |
| 113 | + packageFolder |
| 114 | + ); |
| 115 | + |
| 116 | + result.on('close', (code) => { |
| 117 | + if (code) { |
| 118 | + console.log(colors.red(`The script failed with exit code ${code}`)); |
| 119 | + } |
| 120 | + |
| 121 | + // Pass along the exit code of the child process |
| 122 | + process.exitCode = code || 0; |
| 123 | + }); |
| 124 | + } catch (error) { |
| 125 | + console.log(colors.red('Error: ' + error.message)); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + private static _showUsage(packageJson: IPackageJson, projectCommandSet: ProjectCommandSet): void { |
| 130 | + console.log('usage: rushx [-h]'); |
| 131 | + console.log(' rushx <command> ...' + os.EOL); |
| 132 | + |
| 133 | + console.log('Optional arguments:'); |
| 134 | + console.log(' -h, --help Show this help message and exit.' + os.EOL); |
| 135 | + |
| 136 | + if (projectCommandSet.commandNames.length > 0) { |
| 137 | + console.log(`Project commands for ${colors.cyan(packageJson.name)}:`); |
| 138 | + |
| 139 | + // Calculate the length of the longest script name, for formatting |
| 140 | + let maxLength: number = 0; |
| 141 | + for (const commandName of projectCommandSet.commandNames) { |
| 142 | + maxLength = Math.max(maxLength, commandName.length); |
| 143 | + } |
| 144 | + |
| 145 | + for (const commandName of projectCommandSet.commandNames) { |
| 146 | + const escapedScriptBody: string = JSON.stringify(projectCommandSet.getScriptBody(commandName)); |
| 147 | + |
| 148 | + // The length of the string e.g. " command: " |
| 149 | + const firstPartLength: number = 2 + maxLength + 2; |
| 150 | + // The length for truncating the escaped escapedScriptBody so it doesn't wrap |
| 151 | + // to the next line |
| 152 | + const truncateLength: number = Math.max(0, Utilities.getConsoleWidth() - firstPartLength) - 1; |
| 153 | + |
| 154 | + console.log( |
| 155 | + // Example: " command: " |
| 156 | + ' ' + colors.cyan(Text.padEnd(commandName + ':', maxLength + 2)) |
| 157 | + // Example: "do some thin..." |
| 158 | + + Text.truncateWithEllipsis(escapedScriptBody, truncateLength) |
| 159 | + ); |
| 160 | + } |
| 161 | + |
| 162 | + if (projectCommandSet.malformedScriptNames.length > 0) { |
| 163 | + console.log(os.EOL + colors.yellow('Warning: Some "scripts" entries in the package.json file' |
| 164 | + + ' have malformed names: ' |
| 165 | + + projectCommandSet.malformedScriptNames.map(x => `"${x}"`).join(', '))); |
| 166 | + } |
| 167 | + } else { |
| 168 | + console.log(colors.yellow('Warning: No commands are defined yet for this project.')); |
| 169 | + console.log('You can define a command by adding a "scripts" table to the project\'s package.json file.'); |
| 170 | + } |
| 171 | + } |
| 172 | +} |
0 commit comments