microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bead6d153df3e7bd00100e72ae51a740bba955e6

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

common/scripts/install-run.js

500lines · modecode

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.
4var __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}));
15var __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});
20var __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};
27Object.defineProperty(exports, "__esModule", { value: true });
28exports.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/
39const childProcess = __importStar(require("child_process"));
40const fs = __importStar(require("fs"));
41const os = __importStar(require("os"));
42const path = __importStar(require("path"));
43exports.RUSH_JSON_FILENAME = 'rush.json';
44const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';
45const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH';
46const INSTALLED_FLAG_FILENAME = 'installed.flag';
47const NODE_MODULES_FOLDER_NAME = 'node_modules';
48const PACKAGE_JSON_FILENAME = 'package.json';
49/**
50 * Parse a package specifier (in the form of name\@version) into name and version parts.
51 */
52function _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 */
87function _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 */
133function _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}
150let _npmPath = undefined;
151/**
152 * Get the absolute path to the npm executable
153 */
154function 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}
180exports.getNpmPath = getNpmPath;
181function _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 */
194function _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}
210function _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 */
223function _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}
269let _rushJsonFolder;
270/**
271 * Find the absolute path to the folder containing rush.json
272 */
273function 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}
293exports.findRushJsonFolder = findRushJsonFolder;
294/**
295 * Detects if the package in the specified directory is installed
296 */
297function _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 */
313function _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 */
329function _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}
351function _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 */
373function _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 */
394function _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 */
402function _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}
411function 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}
459exports.installAndRun = installAndRun;
460function 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}
470exports.runWithErrorAndStatusCode = runWithErrorAndStatusCode;
471function _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