From a618fb33ddfc3646ac8d5e2391ea1ee6e208b709 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:23:06 +0100 Subject: [PATCH 1/2] feat(js-lib): Add `sourceMaps.inject()` for injecting debug IDs --- lib/index.ts | 4 + lib/releases/index.ts | 1 + lib/sourceMaps/__tests__/index.test.js | 161 +++++++++++++++++++++++++ lib/sourceMaps/index.ts | 81 +++++++++++++ lib/sourceMaps/options/inject.ts | 23 ++++ lib/types.ts | 29 +++++ 6 files changed, 299 insertions(+) create mode 100644 lib/sourceMaps/__tests__/index.test.js create mode 100644 lib/sourceMaps/index.ts create mode 100644 lib/sourceMaps/options/inject.ts diff --git a/lib/index.ts b/lib/index.ts index 1246c80d3b..2be8e3149b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -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 { @@ -11,6 +12,7 @@ export type { SourceMapsPathDescriptor, SentryCliNewDeployOptions, SentryCliCommitsOptions, + SentryCliInjectOptions, } from './types'; /** @@ -30,6 +32,7 @@ export type { */ export class SentryCli { public releases: Releases; + public sourceMaps: SourceMaps; /** * Creates a new `SentryCli` instance. @@ -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); } /** diff --git a/lib/releases/index.ts b/lib/releases/index.ts index 6cd58c3801..c06612d83d 100644 --- a/lib/releases/index.ts +++ b/lib/releases/index.ts @@ -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()` async uploadSourceMaps( release: string, options: SentryCliUploadSourceMapsOptions diff --git a/lib/sourceMaps/__tests__/index.test.js b/lib/sourceMaps/__tests__/index.test.js new file mode 100644 index 0000000000..d22724c398 --- /dev/null +++ b/lib/sourceMaps/__tests__/index.test.js @@ -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.' + ); + }); + }); + }); +}); diff --git a/lib/sourceMaps/index.ts b/lib/sourceMaps/index.ts new file mode 100644 index 0000000000..b4e9e26f9a --- /dev/null +++ b/lib/sourceMaps/index.ts @@ -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 { + 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 { + return helper.execute(args, live, this.options.silent, this.configFile, this.options); + } +} diff --git a/lib/sourceMaps/options/inject.ts b/lib/sourceMaps/options/inject.ts new file mode 100644 index 0000000000..e0f21c8c50 --- /dev/null +++ b/lib/sourceMaps/options/inject.ts @@ -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; diff --git a/lib/types.ts b/lib/types.ts index c33d9c66f7..e4d123a8f7 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -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; +} From 8c4f7057caf1b1309a3c21d22238ca99b741d583 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:02:44 +0100 Subject: [PATCH 2/2] add changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed6d65a09..dbf221cc71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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