Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Add `sourceMaps.inject()` for injecting debug IDs ([#3088](https://github.com/getsentry/sentry-cli/pull/3088))

## 3.1.0

### New Features
Expand Down
4 changes: 4 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as pkgInfo from '../package.json';
import * as helper from './helper';
import { Releases } from './releases';
import { SourceMaps } from './sourceMaps';
import type { SentryCliOptions } from './types';

export type {
Expand All @@ -11,6 +12,7 @@ export type {
SourceMapsPathDescriptor,
SentryCliNewDeployOptions,
SentryCliCommitsOptions,
SentryCliInjectOptions,
} from './types';

/**
Expand All @@ -30,6 +32,7 @@ export type {
*/
export class SentryCli {
public releases: Releases;
public sourceMaps: SourceMaps;

/**
* Creates a new `SentryCli` instance.
Expand All @@ -49,6 +52,7 @@ export class SentryCli {
}
this.options = options || { silent: false };
this.releases = new Releases(this.options, configFile);
this.sourceMaps = new SourceMaps(this.options, configFile);
}

/**
Expand Down
1 change: 1 addition & 0 deletions lib/releases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export class Releases {
* @param options Options to configure the source map upload.
* @returns A promise that resolves when the upload has completed successfully.
*/
// TODO: Move `uploadSourceMaps()` from Releases to SourceMaps class as `.upload()`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM, but I think we can leave this command in place to avoid breaking anyone once we move things over.

async uploadSourceMaps(
release: string,
options: SentryCliUploadSourceMapsOptions
Expand Down
161 changes: 161 additions & 0 deletions lib/sourceMaps/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
describe('SentryCli source maps', () => {
afterEach(() => {
jest.resetModules();
});

describe('with mock', () => {
let cli;
let mockExecute;
beforeAll(() => {
mockExecute = jest.fn(async () => {});
jest.doMock('../../helper', () => ({
...jest.requireActual('../../helper'),
execute: mockExecute,
}));
});
beforeEach(() => {
mockExecute.mockClear();
// eslint-disable-next-line global-require
const { SentryCli: SentryCliLocal } = require('../..');
cli = new SentryCliLocal();
});

describe('inject', () => {
test('with single path', async () => {
await cli.sourceMaps.inject({ paths: ['./dist'] });
expect(mockExecute).toHaveBeenCalledWith(
['sourcemaps', 'inject', './dist', '--ignore', 'node_modules'],
true,
false,
undefined,
{ silent: false }
);
});

test('with multiple paths', async () => {
await cli.sourceMaps.inject({ paths: ['./dist', './build'] });
expect(mockExecute).toHaveBeenCalledWith(
['sourcemaps', 'inject', './dist', './build', '--ignore', 'node_modules'],
true,
false,
undefined,
{ silent: false }
);
});

test('with custom ignore patterns', async () => {
await cli.sourceMaps.inject({
paths: ['./dist'],
ignore: ['vendor', '*.test.js'],
});
expect(mockExecute).toHaveBeenCalledWith(
['sourcemaps', 'inject', './dist', '--ignore', 'vendor', '--ignore', '*.test.js'],
true,
false,
undefined,
{ silent: false }
);
});

test('with ignoreFile', async () => {
await cli.sourceMaps.inject({
paths: ['./dist'],
ignoreFile: '.gitignore',
});
expect(mockExecute).toHaveBeenCalledWith(
['sourcemaps', 'inject', './dist', '--ignore-file', '.gitignore'],
true,
false,
undefined,
{ silent: false }
);
});

test('with custom extensions', async () => {
await cli.sourceMaps.inject({
paths: ['./dist'],
ext: ['js', 'mjs', 'cjs'],
});
expect(mockExecute).toHaveBeenCalledWith(
[
'sourcemaps',
'inject',
'./dist',
'--ignore',
'node_modules',
'--ext',
'js',
'--ext',
'mjs',
'--ext',
'cjs',
],
true,
false,
undefined,
{ silent: false }
);
});

test('with dryRun', async () => {
await cli.sourceMaps.inject({
paths: ['./dist'],
dryRun: true,
});
expect(mockExecute).toHaveBeenCalledWith(
['sourcemaps', 'inject', './dist', '--ignore', 'node_modules', '--dry-run'],
true,
false,
undefined,
{ silent: false }
);
});

test('with all options', async () => {
await cli.sourceMaps.inject({
paths: ['./dist', './build'],
ignore: ['vendor'],
ext: ['js', 'mjs'],
dryRun: true,
});
expect(mockExecute).toHaveBeenCalledWith(
[
'sourcemaps',
'inject',
'./dist',
'./build',
'--ignore',
'vendor',
'--ext',
'js',
'--ext',
'mjs',
'--dry-run',
],
true,
false,
undefined,
{ silent: false }
);
});

test('throws error when paths is not provided', async () => {
await expect(cli.sourceMaps.inject({})).rejects.toThrow(
'`options.paths` must be a valid array of paths.'
);
});

test('throws error when paths is not an array', async () => {
await expect(cli.sourceMaps.inject({ paths: './dist' })).rejects.toThrow(
'`options.paths` must be a valid array of paths.'
);
});

test('throws error when paths is empty', async () => {
await expect(cli.sourceMaps.inject({ paths: [] })).rejects.toThrow(
'`options.paths` must contain at least one path.'
);
});
});
});
});
81 changes: 81 additions & 0 deletions lib/sourceMaps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

import { SentryCliInjectOptions, SentryCliOptions } from '../types';
import { INJECT_OPTIONS } from './options/inject';
import * as helper from '../helper';

/**
* Default arguments for the `--ignore` option.
*/
const DEFAULT_IGNORE: string[] = ['node_modules'];

/**
* Manages source map operations on Sentry.
*/
export class SourceMaps {
constructor(
public options: SentryCliOptions = {},
private configFile: string | null
) {}

/**
* Fixes up JavaScript source files and source maps with debug ids.
*
* For every minified JS source file, a debug id is generated and
* inserted into the file. If the source file references a
* source map and that source map is locally available,
* the debug id will be injected into it as well.
* If the referenced source map already contains a debug id,
* that id is used instead.
*
* @example
* await cli.sourceMaps.inject({
* // required options:
* paths: ['./dist'],
*
* // default options:
* ignore: ['node_modules'], // globs for files to ignore
* ignoreFile: null, // path to a file with ignore rules
* ext: ['js', 'cjs', 'mjs'], // file extensions to consider
* dryRun: false, // don't modify files on disk
* });
*
* @param options Options to configure the debug id injection.
* @returns A promise that resolves when the injection has completed successfully.
*/
async inject(options: SentryCliInjectOptions): Promise<string> {
if (!options || !options.paths || !Array.isArray(options.paths)) {
throw new Error('`options.paths` must be a valid array of paths.');
}

if (options.paths.length === 0) {
throw new Error('`options.paths` must contain at least one path.');
}

const newOptions = { ...options };
if (!newOptions.ignoreFile && !newOptions.ignore) {
newOptions.ignore = DEFAULT_IGNORE;
}

const args = helper.prepareCommand(
['sourcemaps', 'inject', ...options.paths],
INJECT_OPTIONS,
newOptions
);

return this.execute(args, true);
}

/**
* See {helper.execute} docs.
* @param args Command line arguments passed to `sentry-cli`.
* @param live can be set to:
* - `true` to inherit stdio and reject the promise if the command
* exits with a non-zero exit code.
* - `false` to not inherit stdio and return the output as a string.
* @returns A promise that resolves to the standard output.
*/
async execute(args: string[], live: boolean): Promise<string> {
return helper.execute(args, live, this.options.silent, this.configFile, this.options);
}
}
23 changes: 23 additions & 0 deletions lib/sourceMaps/options/inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { OptionsSchema } from '../../helper';

/**
* Schema for the `sourcemaps inject` command.
*/
export const INJECT_OPTIONS = {
ignore: {
param: '--ignore',
type: 'array',
},
ignoreFile: {
param: '--ignore-file',
type: 'string',
},
ext: {
param: '--ext',
type: 'array',
},
dryRun: {
param: '--dry-run',
type: 'boolean',
},
} satisfies OptionsSchema;
29 changes: 29 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,32 @@ export type SentryCliCommitsOptions = {
*/
ignoreEmpty?: boolean;
}

/**
* Options for injecting debug IDs into source files and source maps
*/
export type SentryCliInjectOptions = {
/**
* One or more paths that Sentry CLI should scan recursively for JavaScript source files.
*/
paths: string[];
/**
* One or more paths to ignore during injection. Overrides entries in ignoreFile file.
* Defaults to ['node_modules'] if neither ignore nor ignoreFile is specified.
*/
ignore?: string[];
/**
* Path to a file containing list of files/directories to ignore.
* Can point to .gitignore or anything with the same format.
*/
ignoreFile?: string;
/**
* Set the file extensions of JavaScript files that are considered for injection.
* This overrides the default extensions (js, cjs, mjs).
*/
ext?: string[];
/**
* Don't modify files on disk.
*/
dryRun?: boolean;
}