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