| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							|  |  |  |  * Copyright Google Inc. All Rights Reserved. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // tslint:disable:no-console
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // TODO(alexeagle): why not import chalk from 'chalk'?
 | 
					
						
							|  |  |  | // Something to do with TS default export in UMD emit...
 | 
					
						
							|  |  |  | const chalk = require('chalk'); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | import * as minimist from 'minimist'; | 
					
						
							|  |  |  | import * as path from 'path'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {SerializationOptions, generateGoldenFile, verifyAgainstGoldenFile} from './main'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const CMD = 'ts-api-guardian'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function startCli() { | 
					
						
							|  |  |  |   const {argv, mode, errors} = parseArguments(process.argv.slice(2)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const options: SerializationOptions = { | 
					
						
							| 
									
										
										
										
											2018-09-19 13:40:21 +02:00
										 |  |  |     stripExportPattern: [].concat(argv['stripExportPattern']), | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |     allowModuleIdentifiers: [].concat(argv['allowModuleIdentifiers']), | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-28 19:05:20 +01:00
										 |  |  |   // Since the API guardian can be also used by other projects, we should not set up the default
 | 
					
						
							|  |  |  |   // Angular project tag rules unless specified explicitly through a given option.
 | 
					
						
							|  |  |  |   if (argv['useAngularTagRules']) { | 
					
						
							|  |  |  |     options.exportTags = { | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |       requireAtLeastOne: ['publicApi', 'codeGenApi'], | 
					
						
							| 
									
										
										
										
											2018-10-28 19:05:20 +01:00
										 |  |  |       banned: ['experimental'], | 
					
						
							|  |  |  |       toCopy: ['deprecated'] | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     options.memberTags = { | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |       requireAtLeastOne: [], | 
					
						
							|  |  |  |       banned: ['experimental', 'publicApi', 'codeGenApi'], | 
					
						
							| 
									
										
										
										
											2018-10-28 19:05:20 +01:00
										 |  |  |       toCopy: ['deprecated'] | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     options.paramTags = { | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |       requireAtLeastOne: [], | 
					
						
							|  |  |  |       banned: ['experimental', 'publicApi', 'codeGenApi'], | 
					
						
							| 
									
										
										
										
											2018-10-28 19:05:20 +01:00
										 |  |  |       toCopy: ['deprecated'] | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   for (const error of errors) { | 
					
						
							|  |  |  |     console.warn(error); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (mode === 'help') { | 
					
						
							|  |  |  |     printUsageAndExit(!!errors.length); | 
					
						
							|  |  |  |   } else { | 
					
						
							| 
									
										
										
										
											2018-10-29 21:25:00 +01:00
										 |  |  |     const targets = resolveFileNamePairs(argv, mode); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (mode === 'out') { | 
					
						
							|  |  |  |       for (const {entrypoint, goldenFile} of targets) { | 
					
						
							|  |  |  |         generateGoldenFile(entrypoint, goldenFile, options); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } else {  // mode === 'verify'
 | 
					
						
							|  |  |  |       let hasDiff = false; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       for (const {entrypoint, goldenFile} of targets) { | 
					
						
							|  |  |  |         const diff = verifyAgainstGoldenFile(entrypoint, goldenFile, options); | 
					
						
							|  |  |  |         if (diff) { | 
					
						
							|  |  |  |           hasDiff = true; | 
					
						
							|  |  |  |           const lines = diff.split('\n'); | 
					
						
							|  |  |  |           if (lines.length) { | 
					
						
							|  |  |  |             lines.pop();  // Remove trailing newline
 | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           for (const line of lines) { | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |             const chalkMap: {[key: string]: | 
					
						
							|  |  |  |                                  any} = {'-': chalk.red, '+': chalk.green, '@': chalk.cyan}; | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |             const chalkFunc = chalkMap[line[0]] || chalk.reset; | 
					
						
							|  |  |  |             console.log(chalkFunc(line)); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (hasDiff) { | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |         // Under bazel, give instructions how to use bazel run to accept the golden file.
 | 
					
						
							|  |  |  |         if (!!process.env['BAZEL_TARGET']) { | 
					
						
							|  |  |  |           console.error('\n\nAccept the new golden file:'); | 
					
						
							| 
									
										
										
										
											2018-10-22 13:52:28 -07:00
										 |  |  |           console.error(`  bazel run ${process.env['BAZEL_TARGET'].replace(/_bin$/, "")}.accept`); | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |         process.exit(1); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function parseArguments(input: string[]): | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |     {argv: minimist.ParsedArgs, mode: string, errors: string[]} { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   let help = false; | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |   const errors: string[] = []; | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const argv = minimist(input, { | 
					
						
							|  |  |  |     string: [ | 
					
						
							|  |  |  |       'out', 'outDir', 'verify', 'verifyDir', 'rootDir', 'stripExportPattern', | 
					
						
							| 
									
										
										
										
											2018-04-04 21:31:34 +01:00
										 |  |  |       'allowModuleIdentifiers' | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |     ], | 
					
						
							|  |  |  |     boolean: [ | 
					
						
							| 
									
										
										
										
											2018-10-28 19:05:20 +01:00
										 |  |  |       'help', 'useAngularTagRules', | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |       // Options used by chalk automagically
 | 
					
						
							|  |  |  |       'color', 'no-color' | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     alias: {'outFile': 'out', 'verifyFile': 'verify'}, | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |     unknown: (option: string) => { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |       if (option[0] === '-') { | 
					
						
							|  |  |  |         errors.push(`Unknown option: ${option}`); | 
					
						
							|  |  |  |         help = true; | 
					
						
							|  |  |  |         return false;  // do not add to argv._
 | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         return true;  // add to argv._
 | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   help = help || argv['help']; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (help) { | 
					
						
							|  |  |  |     return {argv, mode: 'help', errors}; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let modes: string[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (argv['out']) { | 
					
						
							|  |  |  |     modes.push('out'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (argv['outDir']) { | 
					
						
							|  |  |  |     modes.push('out'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (argv['verify']) { | 
					
						
							|  |  |  |     modes.push('verify'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (argv['verifyDir']) { | 
					
						
							|  |  |  |     modes.push('verify'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!argv._.length) { | 
					
						
							|  |  |  |     errors.push('No input file specified.'); | 
					
						
							|  |  |  |     modes = ['help']; | 
					
						
							|  |  |  |   } else if (modes.length !== 1) { | 
					
						
							|  |  |  |     errors.push('Specify either --out[Dir] or --verify[Dir]'); | 
					
						
							|  |  |  |     modes = ['help']; | 
					
						
							|  |  |  |   } else if (argv._.length > 1 && !argv['outDir'] && !argv['verifyDir']) { | 
					
						
							|  |  |  |     errors.push(`More than one input specified. Use --${modes[0]}Dir instead.`); | 
					
						
							|  |  |  |     modes = ['help']; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return {argv, mode: modes[0], errors}; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function printUsageAndExit(error = false) { | 
					
						
							|  |  |  |   const print = error ? console.warn.bind(console) : console.log.bind(console); | 
					
						
							|  |  |  |   print(`Usage:  ${CMD} [options] <file ...>
 | 
					
						
							|  |  |  |         ${CMD} --out <output file> <entrypoint .d.ts file> | 
					
						
							|  |  |  |         ${CMD} --outDir <output dir> [--rootDir .] <entrypoint .d.ts files> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ${CMD} --verify <golden file> <entrypoint .d.ts file> | 
					
						
							|  |  |  |         ${CMD} --verifyDir <golden file dir> [--rootDir .] <entrypoint .d.ts files> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Options: | 
					
						
							|  |  |  |         --help                          Show this usage message | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         --out <file>                    Write golden output to file | 
					
						
							|  |  |  |         --outDir <dir>                  Write golden file structure to directory | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         --verify <file>                 Read golden input from file | 
					
						
							|  |  |  |         --verifyDir <dir>               Read golden file structure from directory | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         --rootDir <dir>                 Specify the root directory of input files | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-29 11:20:17 -07:00
										 |  |  |         --useAngularTagRules <boolean>  Whether the Angular specific tag rules should be used. | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |         --stripExportPattern <regexp>   Do not output exports matching the pattern | 
					
						
							|  |  |  |         --allowModuleIdentifiers <identifier> | 
					
						
							| 
									
										
										
										
											2019-03-29 11:20:17 -07:00
										 |  |  |                                         Allow identifier for "* as foo" imports`);
 | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   process.exit(error ? 1 : 0); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-29 21:25:00 +01:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Resolves a given path to the associated relative path if the current process runs within | 
					
						
							|  |  |  |  * Bazel. We need to use the wrapped NodeJS resolve logic in order to properly handle the given | 
					
						
							|  |  |  |  * runfiles files which are only part of the runfile manifest on Windows. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function resolveBazelFilePath(fileName: string): string { | 
					
						
							|  |  |  |   // If the CLI has been launched through the NodeJS Bazel rules, we need to resolve the
 | 
					
						
							|  |  |  |   // actual file paths because otherwise this script won't work on Windows where runfiles
 | 
					
						
							|  |  |  |   // are not available in the working directory. In order to resolve the real path for the
 | 
					
						
							|  |  |  |   // runfile, we need to use `require.resolve` which handles runfiles properly on Windows.
 | 
					
						
							|  |  |  |   if (process.env['BAZEL_TARGET']) { | 
					
						
							| 
									
										
										
										
											2018-11-01 11:48:18 +01:00
										 |  |  |     return path.relative(process.cwd(), require.resolve(fileName)); | 
					
						
							| 
									
										
										
										
											2018-10-29 21:25:00 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return fileName; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function resolveFileNamePairs( | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |     argv: minimist.ParsedArgs, mode: string): {entrypoint: string, goldenFile: string}[] { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   if (argv[mode]) { | 
					
						
							| 
									
										
										
										
											2018-10-29 21:25:00 +01:00
										 |  |  |     return [{ | 
					
						
							|  |  |  |       entrypoint: resolveBazelFilePath(argv._[0]), | 
					
						
							|  |  |  |       goldenFile: resolveBazelFilePath(argv[mode]), | 
					
						
							|  |  |  |     }]; | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   } else {  // argv[mode + 'Dir']
 | 
					
						
							|  |  |  |     let rootDir = argv['rootDir'] || '.'; | 
					
						
							|  |  |  |     const goldenDir = argv[mode + 'Dir']; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |     return argv._.map((fileName: string) => { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |       return { | 
					
						
							| 
									
										
										
										
											2018-10-29 21:25:00 +01:00
										 |  |  |         entrypoint: resolveBazelFilePath(fileName), | 
					
						
							|  |  |  |         goldenFile: resolveBazelFilePath(path.join(goldenDir, path.relative(rootDir, fileName))), | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |       }; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |