install-run.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. "use strict";
  2. // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
  3. // See the @microsoft/rush package's LICENSE file for license information.
  4. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
  5. if (k2 === undefined) k2 = k;
  6. var desc = Object.getOwnPropertyDescriptor(m, k);
  7. if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
  8. desc = { enumerable: true, get: function() { return m[k]; } };
  9. }
  10. Object.defineProperty(o, k2, desc);
  11. }) : (function(o, m, k, k2) {
  12. if (k2 === undefined) k2 = k;
  13. o[k2] = m[k];
  14. }));
  15. var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
  16. Object.defineProperty(o, "default", { enumerable: true, value: v });
  17. }) : function(o, v) {
  18. o["default"] = v;
  19. });
  20. var __importStar = (this && this.__importStar) || function (mod) {
  21. if (mod && mod.__esModule) return mod;
  22. var result = {};
  23. if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
  24. __setModuleDefault(result, mod);
  25. return result;
  26. };
  27. Object.defineProperty(exports, "__esModule", { value: true });
  28. exports.runWithErrorAndStatusCode = exports.installAndRun = exports.findRushJsonFolder = exports.getNpmPath = exports.RUSH_JSON_FILENAME = void 0;
  29. // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
  30. //
  31. // This script is intended for usage in an automated build environment where a Node tool may not have
  32. // been preinstalled, or may have an unpredictable version. This script will automatically install the specified
  33. // version of the specified tool (if not already installed), and then pass a command-line to it.
  34. // An example usage would be:
  35. //
  36. // node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io
  37. //
  38. // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
  39. const childProcess = __importStar(require("child_process"));
  40. const fs = __importStar(require("fs"));
  41. const os = __importStar(require("os"));
  42. const path = __importStar(require("path"));
  43. exports.RUSH_JSON_FILENAME = 'rush.json';
  44. const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';
  45. const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH';
  46. const INSTALLED_FLAG_FILENAME = 'installed.flag';
  47. const NODE_MODULES_FOLDER_NAME = 'node_modules';
  48. const PACKAGE_JSON_FILENAME = 'package.json';
  49. /**
  50. * Parse a package specifier (in the form of name\@version) into name and version parts.
  51. */
  52. function _parsePackageSpecifier(rawPackageSpecifier) {
  53. rawPackageSpecifier = (rawPackageSpecifier || '').trim();
  54. const separatorIndex = rawPackageSpecifier.lastIndexOf('@');
  55. let name;
  56. let version = undefined;
  57. if (separatorIndex === 0) {
  58. // The specifier starts with a scope and doesn't have a version specified
  59. name = rawPackageSpecifier;
  60. }
  61. else if (separatorIndex === -1) {
  62. // The specifier doesn't have a version
  63. name = rawPackageSpecifier;
  64. }
  65. else {
  66. name = rawPackageSpecifier.substring(0, separatorIndex);
  67. version = rawPackageSpecifier.substring(separatorIndex + 1);
  68. }
  69. if (!name) {
  70. throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);
  71. }
  72. return { name, version };
  73. }
  74. /**
  75. * As a workaround, copyAndTrimNpmrcFile() copies the .npmrc file to the target folder, and also trims
  76. * unusable lines from the .npmrc file.
  77. *
  78. * Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in
  79. * the .npmrc file to provide different authentication tokens for different registry.
  80. * However, if the environment variable is undefined, it expands to an empty string, which
  81. * produces a valid-looking mapping with an invalid URL that causes an error. Instead,
  82. * we'd prefer to skip that line and continue looking in other places such as the user's
  83. * home directory.
  84. *
  85. * IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities.copyAndTrimNpmrcFile()
  86. */
  87. function _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath) {
  88. logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose
  89. logger.info(` --> "${targetNpmrcPath}"`);
  90. let npmrcFileLines = fs.readFileSync(sourceNpmrcPath).toString().split('\n');
  91. npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
  92. const resultLines = [];
  93. // This finds environment variable tokens that look like "${VAR_NAME}"
  94. const expansionRegExp = /\$\{([^\}]+)\}/g;
  95. // Comment lines start with "#" or ";"
  96. const commentRegExp = /^\s*[#;]/;
  97. // Trim out lines that reference environment variables that aren't defined
  98. for (const line of npmrcFileLines) {
  99. let lineShouldBeTrimmed = false;
  100. // Ignore comment lines
  101. if (!commentRegExp.test(line)) {
  102. const environmentVariables = line.match(expansionRegExp);
  103. if (environmentVariables) {
  104. for (const token of environmentVariables) {
  105. // Remove the leading "${" and the trailing "}" from the token
  106. const environmentVariableName = token.substring(2, token.length - 1);
  107. // Is the environment variable defined?
  108. if (!process.env[environmentVariableName]) {
  109. // No, so trim this line
  110. lineShouldBeTrimmed = true;
  111. break;
  112. }
  113. }
  114. }
  115. }
  116. if (lineShouldBeTrimmed) {
  117. // Example output:
  118. // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
  119. resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
  120. }
  121. else {
  122. resultLines.push(line);
  123. }
  124. }
  125. fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
  126. }
  127. /**
  128. * syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file.
  129. * If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder.
  130. *
  131. * IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc()
  132. */
  133. function _syncNpmrc(logger, sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish) {
  134. const sourceNpmrcPath = path.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
  135. const targetNpmrcPath = path.join(targetNpmrcFolder, '.npmrc');
  136. try {
  137. if (fs.existsSync(sourceNpmrcPath)) {
  138. _copyAndTrimNpmrcFile(logger, sourceNpmrcPath, targetNpmrcPath);
  139. }
  140. else if (fs.existsSync(targetNpmrcPath)) {
  141. // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
  142. logger.info(`Deleting ${targetNpmrcPath}`); // Verbose
  143. fs.unlinkSync(targetNpmrcPath);
  144. }
  145. }
  146. catch (e) {
  147. throw new Error(`Error syncing .npmrc file: ${e}`);
  148. }
  149. }
  150. let _npmPath = undefined;
  151. /**
  152. * Get the absolute path to the npm executable
  153. */
  154. function getNpmPath() {
  155. if (!_npmPath) {
  156. try {
  157. if (os.platform() === 'win32') {
  158. // We're on Windows
  159. const whereOutput = childProcess.execSync('where npm', { stdio: [] }).toString();
  160. const lines = whereOutput.split(os.EOL).filter((line) => !!line);
  161. // take the last result, we are looking for a .cmd command
  162. // see https://github.com/microsoft/rushstack/issues/759
  163. _npmPath = lines[lines.length - 1];
  164. }
  165. else {
  166. // We aren't on Windows - assume we're on *NIX or Darwin
  167. _npmPath = childProcess.execSync('command -v npm', { stdio: [] }).toString();
  168. }
  169. }
  170. catch (e) {
  171. throw new Error(`Unable to determine the path to the NPM tool: ${e}`);
  172. }
  173. _npmPath = _npmPath.trim();
  174. if (!fs.existsSync(_npmPath)) {
  175. throw new Error('The NPM executable does not exist');
  176. }
  177. }
  178. return _npmPath;
  179. }
  180. exports.getNpmPath = getNpmPath;
  181. function _ensureFolder(folderPath) {
  182. if (!fs.existsSync(folderPath)) {
  183. const parentDir = path.dirname(folderPath);
  184. _ensureFolder(parentDir);
  185. fs.mkdirSync(folderPath);
  186. }
  187. }
  188. /**
  189. * Create missing directories under the specified base directory, and return the resolved directory.
  190. *
  191. * Does not support "." or ".." path segments.
  192. * Assumes the baseFolder exists.
  193. */
  194. function _ensureAndJoinPath(baseFolder, ...pathSegments) {
  195. let joinedPath = baseFolder;
  196. try {
  197. for (let pathSegment of pathSegments) {
  198. pathSegment = pathSegment.replace(/[\\\/]/g, '+');
  199. joinedPath = path.join(joinedPath, pathSegment);
  200. if (!fs.existsSync(joinedPath)) {
  201. fs.mkdirSync(joinedPath);
  202. }
  203. }
  204. }
  205. catch (e) {
  206. throw new Error(`Error building local installation folder (${path.join(baseFolder, ...pathSegments)}): ${e}`);
  207. }
  208. return joinedPath;
  209. }
  210. function _getRushTempFolder(rushCommonFolder) {
  211. const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME];
  212. if (rushTempFolder !== undefined) {
  213. _ensureFolder(rushTempFolder);
  214. return rushTempFolder;
  215. }
  216. else {
  217. return _ensureAndJoinPath(rushCommonFolder, 'temp');
  218. }
  219. }
  220. /**
  221. * Resolve a package specifier to a static version
  222. */
  223. function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) {
  224. if (!version) {
  225. version = '*'; // If no version is specified, use the latest version
  226. }
  227. if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) {
  228. // If the version contains only characters that we recognize to be used in static version specifiers,
  229. // pass the version through
  230. return version;
  231. }
  232. else {
  233. // version resolves to
  234. try {
  235. const rushTempFolder = _getRushTempFolder(rushCommonFolder);
  236. const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
  237. _syncNpmrc(logger, sourceNpmrcFolder, rushTempFolder);
  238. const npmPath = getNpmPath();
  239. // This returns something that looks like:
  240. // @microsoft/rush@3.0.0 '3.0.0'
  241. // @microsoft/rush@3.0.1 '3.0.1'
  242. // ...
  243. // @microsoft/rush@3.0.20 '3.0.20'
  244. // <blank line>
  245. const npmVersionSpawnResult = childProcess.spawnSync(npmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier'], {
  246. cwd: rushTempFolder,
  247. stdio: []
  248. });
  249. if (npmVersionSpawnResult.status !== 0) {
  250. throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
  251. }
  252. const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();
  253. const versionLines = npmViewVersionOutput.split('\n').filter((line) => !!line);
  254. const latestVersion = versionLines[versionLines.length - 1];
  255. if (!latestVersion) {
  256. throw new Error('No versions found for the specified version range.');
  257. }
  258. const versionMatches = latestVersion.match(/^.+\s\'(.+)\'$/);
  259. if (!versionMatches) {
  260. throw new Error(`Invalid npm output ${latestVersion}`);
  261. }
  262. return versionMatches[1];
  263. }
  264. catch (e) {
  265. throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);
  266. }
  267. }
  268. }
  269. let _rushJsonFolder;
  270. /**
  271. * Find the absolute path to the folder containing rush.json
  272. */
  273. function findRushJsonFolder() {
  274. if (!_rushJsonFolder) {
  275. let basePath = __dirname;
  276. let tempPath = __dirname;
  277. do {
  278. const testRushJsonPath = path.join(basePath, exports.RUSH_JSON_FILENAME);
  279. if (fs.existsSync(testRushJsonPath)) {
  280. _rushJsonFolder = basePath;
  281. break;
  282. }
  283. else {
  284. basePath = tempPath;
  285. }
  286. } while (basePath !== (tempPath = path.dirname(basePath))); // Exit the loop when we hit the disk root
  287. if (!_rushJsonFolder) {
  288. throw new Error('Unable to find rush.json.');
  289. }
  290. }
  291. return _rushJsonFolder;
  292. }
  293. exports.findRushJsonFolder = findRushJsonFolder;
  294. /**
  295. * Detects if the package in the specified directory is installed
  296. */
  297. function _isPackageAlreadyInstalled(packageInstallFolder) {
  298. try {
  299. const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
  300. if (!fs.existsSync(flagFilePath)) {
  301. return false;
  302. }
  303. const fileContents = fs.readFileSync(flagFilePath).toString();
  304. return fileContents.trim() === process.version;
  305. }
  306. catch (e) {
  307. return false;
  308. }
  309. }
  310. /**
  311. * Delete a file. Fail silently if it does not exist.
  312. */
  313. function _deleteFile(file) {
  314. try {
  315. fs.unlinkSync(file);
  316. }
  317. catch (err) {
  318. if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
  319. throw err;
  320. }
  321. }
  322. }
  323. /**
  324. * Removes the following files and directories under the specified folder path:
  325. * - installed.flag
  326. * -
  327. * - node_modules
  328. */
  329. function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) {
  330. try {
  331. const flagFile = path.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
  332. _deleteFile(flagFile);
  333. const packageLockFile = path.resolve(packageInstallFolder, 'package-lock.json');
  334. if (lockFilePath) {
  335. fs.copyFileSync(lockFilePath, packageLockFile);
  336. }
  337. else {
  338. // Not running `npm ci`, so need to cleanup
  339. _deleteFile(packageLockFile);
  340. const nodeModulesFolder = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
  341. if (fs.existsSync(nodeModulesFolder)) {
  342. const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler');
  343. fs.renameSync(nodeModulesFolder, path.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`));
  344. }
  345. }
  346. }
  347. catch (e) {
  348. throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
  349. }
  350. }
  351. function _createPackageJson(packageInstallFolder, name, version) {
  352. try {
  353. const packageJsonContents = {
  354. name: 'ci-rush',
  355. version: '0.0.0',
  356. dependencies: {
  357. [name]: version
  358. },
  359. description: "DON'T WARN",
  360. repository: "DON'T WARN",
  361. license: 'MIT'
  362. };
  363. const packageJsonPath = path.join(packageInstallFolder, PACKAGE_JSON_FILENAME);
  364. fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));
  365. }
  366. catch (e) {
  367. throw new Error(`Unable to create package.json: ${e}`);
  368. }
  369. }
  370. /**
  371. * Run "npm install" in the package install folder.
  372. */
  373. function _installPackage(logger, packageInstallFolder, name, version, command) {
  374. try {
  375. logger.info(`Installing ${name}...`);
  376. const npmPath = getNpmPath();
  377. const result = childProcess.spawnSync(npmPath, [command], {
  378. stdio: 'inherit',
  379. cwd: packageInstallFolder,
  380. env: process.env
  381. });
  382. if (result.status !== 0) {
  383. throw new Error(`"npm ${command}" encountered an error`);
  384. }
  385. logger.info(`Successfully installed ${name}@${version}`);
  386. }
  387. catch (e) {
  388. throw new Error(`Unable to install package: ${e}`);
  389. }
  390. }
  391. /**
  392. * Get the ".bin" path for the package.
  393. */
  394. function _getBinPath(packageInstallFolder, binName) {
  395. const binFolderPath = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
  396. const resolvedBinName = os.platform() === 'win32' ? `${binName}.cmd` : binName;
  397. return path.resolve(binFolderPath, resolvedBinName);
  398. }
  399. /**
  400. * Write a flag file to the package's install directory, signifying that the install was successful.
  401. */
  402. function _writeFlagFile(packageInstallFolder) {
  403. try {
  404. const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
  405. fs.writeFileSync(flagFilePath, process.version);
  406. }
  407. catch (e) {
  408. throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
  409. }
  410. }
  411. function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) {
  412. const rushJsonFolder = findRushJsonFolder();
  413. const rushCommonFolder = path.join(rushJsonFolder, 'common');
  414. const rushTempFolder = _getRushTempFolder(rushCommonFolder);
  415. const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`);
  416. if (!_isPackageAlreadyInstalled(packageInstallFolder)) {
  417. // The package isn't already installed
  418. _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath);
  419. const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
  420. _syncNpmrc(logger, sourceNpmrcFolder, packageInstallFolder);
  421. _createPackageJson(packageInstallFolder, packageName, packageVersion);
  422. const command = lockFilePath ? 'ci' : 'install';
  423. _installPackage(logger, packageInstallFolder, packageName, packageVersion, command);
  424. _writeFlagFile(packageInstallFolder);
  425. }
  426. const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;
  427. const statusMessageLine = new Array(statusMessage.length + 1).join('-');
  428. logger.info(os.EOL + statusMessage + os.EOL + statusMessageLine + os.EOL);
  429. const binPath = _getBinPath(packageInstallFolder, packageBinName);
  430. const binFolderPath = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
  431. // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to
  432. // assign via the process.env proxy to ensure that we append to the right PATH key.
  433. const originalEnvPath = process.env.PATH || '';
  434. let result;
  435. try {
  436. // Node.js on Windows can not spawn a file when the path has a space on it
  437. // unless the path gets wrapped in a cmd friendly way and shell mode is used
  438. const shouldUseShell = binPath.includes(' ') && os.platform() === 'win32';
  439. const platformBinPath = shouldUseShell ? `"${binPath}"` : binPath;
  440. process.env.PATH = [binFolderPath, originalEnvPath].join(path.delimiter);
  441. result = childProcess.spawnSync(platformBinPath, packageBinArgs, {
  442. stdio: 'inherit',
  443. windowsVerbatimArguments: false,
  444. shell: shouldUseShell,
  445. cwd: process.cwd(),
  446. env: process.env
  447. });
  448. }
  449. finally {
  450. process.env.PATH = originalEnvPath;
  451. }
  452. if (result.status !== null) {
  453. return result.status;
  454. }
  455. else {
  456. throw result.error || new Error('An unknown error occurred.');
  457. }
  458. }
  459. exports.installAndRun = installAndRun;
  460. function runWithErrorAndStatusCode(logger, fn) {
  461. process.exitCode = 1;
  462. try {
  463. const exitCode = fn();
  464. process.exitCode = exitCode;
  465. }
  466. catch (e) {
  467. logger.error(os.EOL + os.EOL + e.toString() + os.EOL + os.EOL);
  468. }
  469. }
  470. exports.runWithErrorAndStatusCode = runWithErrorAndStatusCode;
  471. function _run() {
  472. const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;
  473. if (!nodePath) {
  474. throw new Error('Unexpected exception: could not detect node path');
  475. }
  476. if (path.basename(scriptPath).toLowerCase() !== 'install-run.js') {
  477. // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
  478. // to the script that (presumably) imported this file
  479. return;
  480. }
  481. if (process.argv.length < 4) {
  482. console.log('Usage: install-run.js <package>@<version> <command> [args...]');
  483. console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');
  484. process.exit(1);
  485. }
  486. const logger = { info: console.log, error: console.error };
  487. runWithErrorAndStatusCode(logger, () => {
  488. const rushJsonFolder = findRushJsonFolder();
  489. const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common');
  490. const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier);
  491. const name = packageSpecifier.name;
  492. const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier);
  493. if (packageSpecifier.version !== version) {
  494. console.log(`Resolved to ${name}@${version}`);
  495. }
  496. return installAndRun(logger, name, version, packageBinName, packageBinArgs);
  497. });
  498. }
  499. _run();
  500. //# sourceMappingURL=install-run.js.map