Skip to content

Commit c4ca248

Browse files
authored
feat(js-lib): Add sourceMaps.inject() for injecting debug IDs (#3088)
### Description The JS Bundler Plugins repo uses the CLI instance in the Build Plugin Manager. However, injecting debug IDs is handled with a "raw" `.execute()`. This PR adds this directly to the CLI instance under the namespace `sourceMaps`. Uploading source maps can be handled through this namespace as well in the future. related PR: getsentry/sentry-javascript-bundler-plugins#836
1 parent 0686ecc commit c4ca248

7 files changed

Lines changed: 300 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Add `sourceMaps.inject()` for injecting debug IDs ([#3088](https://github.com/getsentry/sentry-cli/pull/3088))
78
- Add `--install-group` parameter to `sentry-cli build upload` for controlling update visibility between builds ([#3094](https://github.com/getsentry/sentry-cli/pull/3094))
89

910
### Fixes

lib/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import * as pkgInfo from '../package.json';
44
import * as helper from './helper';
55
import { Releases } from './releases';
6+
import { SourceMaps } from './sourceMaps';
67
import type { SentryCliOptions } from './types';
78

89
export type {
@@ -11,6 +12,7 @@ export type {
1112
SourceMapsPathDescriptor,
1213
SentryCliNewDeployOptions,
1314
SentryCliCommitsOptions,
15+
SentryCliInjectOptions,
1416
} from './types';
1517

1618
/**
@@ -30,6 +32,7 @@ export type {
3032
*/
3133
export class SentryCli {
3234
public releases: Releases;
35+
public sourceMaps: SourceMaps;
3336

3437
/**
3538
* Creates a new `SentryCli` instance.
@@ -49,6 +52,7 @@ export class SentryCli {
4952
}
5053
this.options = options || { silent: false };
5154
this.releases = new Releases(this.options, configFile);
55+
this.sourceMaps = new SourceMaps(this.options, configFile);
5256
}
5357

5458
/**

lib/releases/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export class Releases {
120120
* @param options Options to configure the source map upload.
121121
* @returns A promise that resolves when the upload has completed successfully.
122122
*/
123+
// TODO: Add `uploadSourceMaps()` to SourceMaps class as `.upload()` (keep it here too for backward compatibility)
123124
async uploadSourceMaps(
124125
release: string,
125126
options: SentryCliUploadSourceMapsOptions
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
describe('SentryCli source maps', () => {
2+
afterEach(() => {
3+
jest.resetModules();
4+
});
5+
6+
describe('with mock', () => {
7+
let cli;
8+
let mockExecute;
9+
beforeAll(() => {
10+
mockExecute = jest.fn(async () => {});
11+
jest.doMock('../../helper', () => ({
12+
...jest.requireActual('../../helper'),
13+
execute: mockExecute,
14+
}));
15+
});
16+
beforeEach(() => {
17+
mockExecute.mockClear();
18+
// eslint-disable-next-line global-require
19+
const { SentryCli: SentryCliLocal } = require('../..');
20+
cli = new SentryCliLocal();
21+
});
22+
23+
describe('inject', () => {
24+
test('with single path', async () => {
25+
await cli.sourceMaps.inject({ paths: ['./dist'] });
26+
expect(mockExecute).toHaveBeenCalledWith(
27+
['sourcemaps', 'inject', './dist', '--ignore', 'node_modules'],
28+
true,
29+
false,
30+
undefined,
31+
{ silent: false }
32+
);
33+
});
34+
35+
test('with multiple paths', async () => {
36+
await cli.sourceMaps.inject({ paths: ['./dist', './build'] });
37+
expect(mockExecute).toHaveBeenCalledWith(
38+
['sourcemaps', 'inject', './dist', './build', '--ignore', 'node_modules'],
39+
true,
40+
false,
41+
undefined,
42+
{ silent: false }
43+
);
44+
});
45+
46+
test('with custom ignore patterns', async () => {
47+
await cli.sourceMaps.inject({
48+
paths: ['./dist'],
49+
ignore: ['vendor', '*.test.js'],
50+
});
51+
expect(mockExecute).toHaveBeenCalledWith(
52+
['sourcemaps', 'inject', './dist', '--ignore', 'vendor', '--ignore', '*.test.js'],
53+
true,
54+
false,
55+
undefined,
56+
{ silent: false }
57+
);
58+
});
59+
60+
test('with ignoreFile', async () => {
61+
await cli.sourceMaps.inject({
62+
paths: ['./dist'],
63+
ignoreFile: '.gitignore',
64+
});
65+
expect(mockExecute).toHaveBeenCalledWith(
66+
['sourcemaps', 'inject', './dist', '--ignore-file', '.gitignore'],
67+
true,
68+
false,
69+
undefined,
70+
{ silent: false }
71+
);
72+
});
73+
74+
test('with custom extensions', async () => {
75+
await cli.sourceMaps.inject({
76+
paths: ['./dist'],
77+
ext: ['js', 'mjs', 'cjs'],
78+
});
79+
expect(mockExecute).toHaveBeenCalledWith(
80+
[
81+
'sourcemaps',
82+
'inject',
83+
'./dist',
84+
'--ignore',
85+
'node_modules',
86+
'--ext',
87+
'js',
88+
'--ext',
89+
'mjs',
90+
'--ext',
91+
'cjs',
92+
],
93+
true,
94+
false,
95+
undefined,
96+
{ silent: false }
97+
);
98+
});
99+
100+
test('with dryRun', async () => {
101+
await cli.sourceMaps.inject({
102+
paths: ['./dist'],
103+
dryRun: true,
104+
});
105+
expect(mockExecute).toHaveBeenCalledWith(
106+
['sourcemaps', 'inject', './dist', '--ignore', 'node_modules', '--dry-run'],
107+
true,
108+
false,
109+
undefined,
110+
{ silent: false }
111+
);
112+
});
113+
114+
test('with all options', async () => {
115+
await cli.sourceMaps.inject({
116+
paths: ['./dist', './build'],
117+
ignore: ['vendor'],
118+
ext: ['js', 'mjs'],
119+
dryRun: true,
120+
});
121+
expect(mockExecute).toHaveBeenCalledWith(
122+
[
123+
'sourcemaps',
124+
'inject',
125+
'./dist',
126+
'./build',
127+
'--ignore',
128+
'vendor',
129+
'--ext',
130+
'js',
131+
'--ext',
132+
'mjs',
133+
'--dry-run',
134+
],
135+
true,
136+
false,
137+
undefined,
138+
{ silent: false }
139+
);
140+
});
141+
142+
test('throws error when paths is not provided', async () => {
143+
await expect(cli.sourceMaps.inject({})).rejects.toThrow(
144+
'`options.paths` must be a valid array of paths.'
145+
);
146+
});
147+
148+
test('throws error when paths is not an array', async () => {
149+
await expect(cli.sourceMaps.inject({ paths: './dist' })).rejects.toThrow(
150+
'`options.paths` must be a valid array of paths.'
151+
);
152+
});
153+
154+
test('throws error when paths is empty', async () => {
155+
await expect(cli.sourceMaps.inject({ paths: [] })).rejects.toThrow(
156+
'`options.paths` must contain at least one path.'
157+
);
158+
});
159+
});
160+
});
161+
});

lib/sourceMaps/index.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict';
2+
3+
import { SentryCliInjectOptions, SentryCliOptions } from '../types';
4+
import { INJECT_OPTIONS } from './options/inject';
5+
import * as helper from '../helper';
6+
7+
/**
8+
* Default arguments for the `--ignore` option.
9+
*/
10+
const DEFAULT_IGNORE: string[] = ['node_modules'];
11+
12+
/**
13+
* Manages source map operations on Sentry.
14+
*/
15+
export class SourceMaps {
16+
constructor(
17+
public options: SentryCliOptions = {},
18+
private configFile: string | null
19+
) {}
20+
21+
/**
22+
* Fixes up JavaScript source files and source maps with debug ids.
23+
*
24+
* For every minified JS source file, a debug id is generated and
25+
* inserted into the file. If the source file references a
26+
* source map and that source map is locally available,
27+
* the debug id will be injected into it as well.
28+
* If the referenced source map already contains a debug id,
29+
* that id is used instead.
30+
*
31+
* @example
32+
* await cli.sourceMaps.inject({
33+
* // required options:
34+
* paths: ['./dist'],
35+
*
36+
* // default options:
37+
* ignore: ['node_modules'], // globs for files to ignore
38+
* ignoreFile: null, // path to a file with ignore rules
39+
* ext: ['js', 'cjs', 'mjs'], // file extensions to consider
40+
* dryRun: false, // don't modify files on disk
41+
* });
42+
*
43+
* @param options Options to configure the debug id injection.
44+
* @returns A promise that resolves when the injection has completed successfully.
45+
*/
46+
async inject(options: SentryCliInjectOptions): Promise<string> {
47+
if (!options || !options.paths || !Array.isArray(options.paths)) {
48+
throw new Error('`options.paths` must be a valid array of paths.');
49+
}
50+
51+
if (options.paths.length === 0) {
52+
throw new Error('`options.paths` must contain at least one path.');
53+
}
54+
55+
const newOptions = { ...options };
56+
if (!newOptions.ignoreFile && !newOptions.ignore) {
57+
newOptions.ignore = DEFAULT_IGNORE;
58+
}
59+
60+
const args = helper.prepareCommand(
61+
['sourcemaps', 'inject', ...options.paths],
62+
INJECT_OPTIONS,
63+
newOptions
64+
);
65+
66+
return this.execute(args, true);
67+
}
68+
69+
/**
70+
* See {helper.execute} docs.
71+
* @param args Command line arguments passed to `sentry-cli`.
72+
* @param live can be set to:
73+
* - `true` to inherit stdio and reject the promise if the command
74+
* exits with a non-zero exit code.
75+
* - `false` to not inherit stdio and return the output as a string.
76+
* @returns A promise that resolves to the standard output.
77+
*/
78+
async execute(args: string[], live: boolean): Promise<string> {
79+
return helper.execute(args, live, this.options.silent, this.configFile, this.options);
80+
}
81+
}

lib/sourceMaps/options/inject.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { OptionsSchema } from '../../helper';
2+
3+
/**
4+
* Schema for the `sourcemaps inject` command.
5+
*/
6+
export const INJECT_OPTIONS = {
7+
ignore: {
8+
param: '--ignore',
9+
type: 'array',
10+
},
11+
ignoreFile: {
12+
param: '--ignore-file',
13+
type: 'string',
14+
},
15+
ext: {
16+
param: '--ext',
17+
type: 'array',
18+
},
19+
dryRun: {
20+
param: '--dry-run',
21+
type: 'boolean',
22+
},
23+
} satisfies OptionsSchema;

lib/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,32 @@ export type SentryCliCommitsOptions = {
205205
*/
206206
ignoreEmpty?: boolean;
207207
}
208+
209+
/**
210+
* Options for injecting debug IDs into source files and source maps
211+
*/
212+
export type SentryCliInjectOptions = {
213+
/**
214+
* One or more paths that Sentry CLI should scan recursively for JavaScript source files.
215+
*/
216+
paths: string[];
217+
/**
218+
* One or more paths to ignore during injection. Overrides entries in ignoreFile file.
219+
* Defaults to ['node_modules'] if neither ignore nor ignoreFile is specified.
220+
*/
221+
ignore?: string[];
222+
/**
223+
* Path to a file containing list of files/directories to ignore.
224+
* Can point to .gitignore or anything with the same format.
225+
*/
226+
ignoreFile?: string;
227+
/**
228+
* Set the file extensions of JavaScript files that are considered for injection.
229+
* This overrides the default extensions (js, cjs, mjs).
230+
*/
231+
ext?: string[];
232+
/**
233+
* Don't modify files on disk.
234+
*/
235+
dryRun?: boolean;
236+
}

0 commit comments

Comments
 (0)