helper.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. 'use strict';
  2. const path = require('path');
  3. const childProcess = require('child_process');
  4. /**
  5. * This convoluted function resolves the path to the `sentry-cli` binary in a
  6. * way that can't be analysed by @vercel/nft.
  7. *
  8. * Without this, the binary can be detected as an asset and included by bundlers
  9. * that use @vercel/nft.
  10. * @returns {string} The path to the sentry-cli binary
  11. */
  12. function getBinaryPath() {
  13. const parts = [];
  14. parts.push(__dirname);
  15. parts.push('..');
  16. parts.push(`sentry-cli${process.platform === 'win32' ? '.exe' : ''}`);
  17. return path.resolve(...parts);
  18. }
  19. /**
  20. * Absolute path to the sentry-cli binary (platform dependent).
  21. * @type {string}
  22. */
  23. let binaryPath = getBinaryPath();
  24. /**
  25. * Overrides the default binary path with a mock value, useful for testing.
  26. *
  27. * @param {string} mockPath The new path to the mock sentry-cli binary
  28. */
  29. function mockBinaryPath(mockPath) {
  30. binaryPath = mockPath;
  31. }
  32. /**
  33. * The javascript type of a command line option.
  34. * @typedef {'array'|'string'|'boolean'|'inverted-boolean'} OptionType
  35. */
  36. /**
  37. * Schema definition of a command line option.
  38. * @typedef {object} OptionSchema
  39. * @prop {string} param The flag of the command line option including dashes.
  40. * @prop {OptionType} type The value type of the command line option.
  41. */
  42. /**
  43. * Schema definition for a command.
  44. * @typedef {Object.<string, OptionSchema>} OptionsSchema
  45. */
  46. /**
  47. * Serializes command line options into an arguments array.
  48. *
  49. * @param {OptionsSchema} schema An options schema required by the command.
  50. * @param {object} options An options object according to the schema.
  51. * @returns {string[]} An arguments array that can be passed via command line.
  52. */
  53. function serializeOptions(schema, options) {
  54. return Object.keys(schema).reduce((newOptions, option) => {
  55. const paramValue = options[option];
  56. if (paramValue === undefined) {
  57. return newOptions;
  58. }
  59. const paramType = schema[option].type;
  60. const paramName = schema[option].param;
  61. if (paramType === 'array') {
  62. if (!Array.isArray(paramValue)) {
  63. throw new Error(`${option} should be an array`);
  64. }
  65. return newOptions.concat(
  66. paramValue.reduce((acc, value) => acc.concat([paramName, String(value)]), [])
  67. );
  68. }
  69. if (paramType === 'boolean') {
  70. if (typeof paramValue !== 'boolean') {
  71. throw new Error(`${option} should be a bool`);
  72. }
  73. const invertedParamName = schema[option].invertedParam;
  74. if (paramValue && paramName !== undefined) {
  75. return newOptions.concat([paramName]);
  76. }
  77. if (!paramValue && invertedParamName !== undefined) {
  78. return newOptions.concat([invertedParamName]);
  79. }
  80. return newOptions;
  81. }
  82. return newOptions.concat(paramName, paramValue);
  83. }, []);
  84. }
  85. /**
  86. * Serializes the command and its options into an arguments array.
  87. *
  88. * @param {string} command The literal name of the command.
  89. * @param {OptionsSchema} [schema] An options schema required by the command.
  90. * @param {object} [options] An options object according to the schema.
  91. * @returns {string[]} An arguments array that can be passed via command line.
  92. */
  93. function prepareCommand(command, schema, options) {
  94. return command.concat(serializeOptions(schema || {}, options || {}));
  95. }
  96. /**
  97. * Returns the absolute path to the `sentry-cli` binary.
  98. * @returns {string}
  99. */
  100. function getPath() {
  101. return binaryPath;
  102. }
  103. /**
  104. * Runs `sentry-cli` with the given command line arguments.
  105. *
  106. * Use {@link prepareCommand} to specify the command and add arguments for command-
  107. * specific options. For top-level options, use {@link serializeOptions} directly.
  108. *
  109. * The returned promise resolves with the standard output of the command invocation
  110. * including all newlines. In order to parse this output, be sure to trim the output
  111. * first.
  112. *
  113. * If the command failed to execute, the Promise rejects with the error returned by the
  114. * CLI. This error includes a `code` property with the process exit status.
  115. *
  116. * @example
  117. * const output = await execute(['--version']);
  118. * expect(output.trim()).toBe('sentry-cli x.y.z');
  119. *
  120. * @param {string[]} args Command line arguments passed to `sentry-cli`.
  121. * @param {boolean} live We inherit stdio to display `sentry-cli` output directly.
  122. * @param {boolean} silent Disable stdout for silents build (CI/Webpack Stats, ...)
  123. * @param {string} [configFile] Relative or absolute path to the configuration file.
  124. * @param {Object} [config] More configuration to pass to the CLI
  125. * @returns {Promise.<string>} A promise that resolves to the standard output.
  126. */
  127. function execute(args, live, silent, configFile, config = {}) {
  128. const env = { ...process.env };
  129. if (configFile) {
  130. env.SENTRY_PROPERTIES = configFile;
  131. }
  132. if (config.url) {
  133. env.SENTRY_URL = config.url;
  134. }
  135. if (config.authToken) {
  136. env.SENTRY_AUTH_TOKEN = config.authToken;
  137. }
  138. if (config.apiKey) {
  139. env.SENTRY_API_KEY = config.apiKey;
  140. }
  141. if (config.dsn) {
  142. env.SENTRY_DSN = config.dsn;
  143. }
  144. if (config.org) {
  145. env.SENTRY_ORG = config.org;
  146. }
  147. if (config.project) {
  148. env.SENTRY_PROJECT = config.project;
  149. }
  150. if (config.vcsRemote) {
  151. env.SENTRY_VCS_REMOTE = config.vcsRemote;
  152. }
  153. if (config.customHeader) {
  154. env.CUSTOM_HEADER = config.customHeader;
  155. }
  156. return new Promise((resolve, reject) => {
  157. if (live === true) {
  158. const output = silent ? 'ignore' : 'inherit';
  159. const pid = childProcess.spawn(getPath(), args, {
  160. env,
  161. // stdin, stdout, stderr
  162. stdio: ['ignore', output, output],
  163. });
  164. pid.on('exit', () => {
  165. resolve();
  166. });
  167. } else {
  168. childProcess.execFile(getPath(), args, { env }, (err, stdout) => {
  169. if (err) {
  170. reject(err);
  171. } else {
  172. resolve(stdout);
  173. }
  174. });
  175. }
  176. });
  177. }
  178. function getProjectFlagsFromOptions({ projects = [] } = {}) {
  179. return projects.reduce((flags, project) => flags.concat('-p', project), []);
  180. }
  181. module.exports = {
  182. execute,
  183. getPath,
  184. getProjectFlagsFromOptions,
  185. mockBinaryPath,
  186. prepareCommand,
  187. serializeOptions,
  188. };