Vulnerability alerts and dependency updates haunt us these days, especially when they surface in user-facing dependencies. Obviously, we want to fix these issues as quickly as possible, though with 20+ alerts popping up every week lately (to be clear, the vast majority of these alerts are dev and test dependencies!), the maintenance burden has reached new levels. So we discussed if we can reduce the number of direct and transitive dependencies we ship in our SDK packages. The goal is that we replace dependencies with much smaller, tailored/custom implementations.
I had cursor run an audit across the dependencies of our SDK packages. It presented a couple of opportunities that, after filtering out a couple of suggestions, I think are worth looking into further:
Replace with Custom Code
1. yargs — @sentry/remix
Transitive dep count: ~10 packages (cliui, escalade, get-caller-file,
require-directory, string-width, y18n, yargs-parser, ansi-regex, strip-ansi,
wrap-ansi, …)
Where it is used:
packages/remix/scripts/sentry-upload-sourcemaps.js — a CLI script that ships via the bin
field. It parses 8 simple --flag / --flag value options with no subcommands, no positional
args, no nesting.
How to replace:
util.parseArgs has been built into Node.js since v18.3.0 and handles everything this script
needs:
// Before
const yargs = require('yargs');
const argv = yargs(process.argv.slice(2))
.option('release', { type: 'string' })
.option('org', { type: 'string' })
.option('project', { type: 'string' })
.option('url', { type: 'string' })
.option('urlPrefix', { type: 'string', default: DEFAULT_URL_PREFIX })
.option('buildPath', { type: 'string', default: DEFAULT_BUILD_PATH })
.option('disableDebugIds', { type: 'boolean', default: false })
.option('deleteAfterUpload', { type: 'boolean', default: true })
.argv;
// After
const { parseArgs } = require('node:util');
const { values: argv } = parseArgs({
args: process.argv.slice(2),
options: {
release: { type: 'string' },
org: { type: 'string' },
project: { type: 'string' },
url: { type: 'string' },
urlPrefix: { type: 'string', default: DEFAULT_URL_PREFIX },
buildPath: { type: 'string', default: DEFAULT_BUILD_PATH },
disableDebugIds: { type: 'boolean', default: false },
deleteAfterUpload: { type: 'boolean', default: true },
},
});
The --usage / --help text can be printed manually on parse error (a plain console.log
string). The script is a simple one-shot tool, so the ergonomics of yargs' help formatter are
not required.
2. glob — @sentry/remix
Transitive dep count: 4 packages (minimatch, brace-expansion, balanced-match,
minipass)
Where it is used:
packages/remix/scripts/deleteSourcemaps.js — finds all **/*.map files under a given build
directory and deletes them:
const mapFiles = glob.sync('**/*.map', { cwd: buildPath });
How to replace:
This is a fully static pattern (**/*.map) over a known root directory. A simple recursive
fs.readdirSync walk is all that is needed:
const fs = require('node:fs');
const path = require('node:path');
function findMapFiles(dir) {
const results = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...findMapFiles(full));
} else if (entry.isFile() && entry.name.endsWith('.map')) {
results.push(full);
}
}
return results;
}
3. glob — @sentry/react-router
Transitive dep count: ~8 packages (jackspeak, minipass, path-scurry, minimatch,
brace-expansion, balanced-match, @pkgjs/parseargs, …)
Where it is used:
packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts — resolves the
filesToDeleteAfterUpload option (typically ["<buildDir>/**/*.map"]) after a Vite build, then
deletes those files:
const filePathsToDelete = await glob(updatedFilesToDeleteAfterUpload, {
absolute: true,
nodir: true,
});
How to replace:
The default value is always a single **/*.map pattern. The custom
filesToDeleteAfterUpload option that users can pass is already typed as string | string[]
matching glob patterns. A small async recursive walker handles the default case; for user-supplied
patterns a lightweight inline matcher is sufficient since **/*.ext and dir/**/*.ext cover
virtually all real-world values:
import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
async function findFiles(dir: string, predicate: (name: string) => boolean): Promise<string[]> {
const results: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...(await findFiles(full, predicate)));
} else if (entry.isFile() && predicate(entry.name)) {
results.push(full);
}
}
return results;
}
Note: @sentry/react-router already requires Node ≥ 20 ("node": ">=20" in its engines
field), so Node 22's native fs.glob could also be used once Node 22 becomes the minimum.
4. minimatch — @sentry/node
Transitive dep count: 2 packages (brace-expansion, balanced-match)
Where it is used:
packages/node/src/integrations/tracing/fastify/fastify-otel/index.js — vendored copy of
@fastify/otel. It performs a single glob match of a Fastify route URL against a user-supplied
ignorePaths pattern:
const globMatcher = minimatch.minimatch;
this[kIgnorePaths] = routeOptions => globMatcher(routeOptions.url, ignorePaths);
How to replace:
Route URLs are simple path strings (e.g. /api/users, /health). Realistic ignorePaths
values are things like /health, /api/*, /static/**. Full brace-expansion glob semantics
are not needed. A minimal path glob matcher (~20 lines) handles * (single segment) and **
(any number of segments):
function matchesGlob(path, pattern) {
// Escape regex special chars except * which we handle ourselves
const regexStr = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*\*/g, '{{DOUBLE_STAR}}')
.replace(/\*/g, '[^/]*')
.replace(/{{DOUBLE_STAR}}/g, '.*');
return new RegExp(`^${regexStr}$`).test(path);
}
Since fastify-otel/index.js is already a vendored file that we maintain (and diverge from
upstream where needed), this change is straightforward.
Replace with a Better Alternative
5. recast + @babel/parser — @sentry/sveltekit
Transitive dep count: ~8 packages combined
recast brings: ast-types, source-map, tslib
@babel/parser brings: @babel/types, @babel/helper-string-parser,
@babel/helper-validator-identifier, to-fast-properties
Where they are used:
packages/sveltekit/src/vite/autoInstrument.ts and recastTypescriptParser.ts. The plugin
reads SvelteKit +page.ts / +layout.server.ts files at build time, parses them as TypeScript
ASTs, and checks whether a top-level export const load or export function load declaration
exists. It does not actually mutate the AST — if load is found, it discards the entire
module and returns a freshly generated wrapper string. Because the AST is only read (never
rewritten in place), recast's write-preserving round-trip feature is never exercised, and
@babel/parser is only used as the parse step.
Better alternative: acorn + acorn-typescript
acorn is the parser used by Node.js itself (0 transitive deps). acorn-typescript adds
TypeScript syntax support as an acorn plugin (0 transitive deps). The existing check only needs
to walk top-level ExportNamedDeclaration nodes, which maps directly onto acorn's AST output.
magic-string (already a dependency of @sentry/sveltekit) continues to handle the
append/prepend operations in injectGlobalValues.ts unchanged.
Dep reduction: Removes recast, ast-types, source-map, @babel/parser, @babel/types,
@babel/helper-string-parser, @babel/helper-validator-identifier, to-fast-properties (~8
packages). Adds acorn + acorn-typescript (0 transitive deps each).
Vulnerability alerts and dependency updates haunt us these days, especially when they surface in user-facing dependencies. Obviously, we want to fix these issues as quickly as possible, though with 20+ alerts popping up every week lately (to be clear, the vast majority of these alerts are dev and test dependencies!), the maintenance burden has reached new levels. So we discussed if we can reduce the number of direct and transitive dependencies we ship in our SDK packages. The goal is that we replace dependencies with much smaller, tailored/custom implementations.
I had cursor run an audit across the dependencies of our SDK packages. It presented a couple of opportunities that, after filtering out a couple of suggestions, I think are worth looking into further:
Replace with Custom Code
1.
yargs—@sentry/remixTransitive dep count: ~10 packages (
cliui,escalade,get-caller-file,require-directory,string-width,y18n,yargs-parser,ansi-regex,strip-ansi,wrap-ansi, …)Where it is used:
packages/remix/scripts/sentry-upload-sourcemaps.js— a CLI script that ships via thebinfield. It parses 8 simple
--flag/--flag valueoptions with no subcommands, no positionalargs, no nesting.
How to replace:
util.parseArgshas been built into Node.js since v18.3.0 and handles everything this scriptneeds:
The
--usage/--helptext can be printed manually on parse error (a plainconsole.logstring). The script is a simple one-shot tool, so the ergonomics of yargs' help formatter are
not required.
2.
glob—@sentry/remixTransitive dep count: 4 packages (
minimatch,brace-expansion,balanced-match,minipass)Where it is used:
packages/remix/scripts/deleteSourcemaps.js— finds all**/*.mapfiles under a given builddirectory and deletes them:
How to replace:
This is a fully static pattern (
**/*.map) over a known root directory. A simple recursivefs.readdirSyncwalk is all that is needed:3.
glob—@sentry/react-routerTransitive dep count: ~8 packages (
jackspeak,minipass,path-scurry,minimatch,brace-expansion,balanced-match,@pkgjs/parseargs, …)Where it is used:
packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts— resolves thefilesToDeleteAfterUploadoption (typically["<buildDir>/**/*.map"]) after a Vite build, thendeletes those files:
How to replace:
The default value is always a single
**/*.mappattern. The customfilesToDeleteAfterUploadoption that users can pass is already typed asstring | string[]matching glob patterns. A small async recursive walker handles the default case; for user-supplied
patterns a lightweight inline matcher is sufficient since
**/*.extanddir/**/*.extcovervirtually all real-world values:
Note:
@sentry/react-routeralready requires Node ≥ 20 ("node": ">=20"in itsenginesfield), so Node 22's native
fs.globcould also be used once Node 22 becomes the minimum.4.
minimatch—@sentry/nodeTransitive dep count: 2 packages (
brace-expansion,balanced-match)Where it is used:
packages/node/src/integrations/tracing/fastify/fastify-otel/index.js— vendored copy of@fastify/otel. It performs a single glob match of a Fastify route URL against a user-suppliedignorePathspattern:How to replace:
Route URLs are simple path strings (e.g.
/api/users,/health). RealisticignorePathsvalues are things like
/health,/api/*,/static/**. Full brace-expansion glob semanticsare not needed. A minimal path glob matcher (~20 lines) handles
*(single segment) and**(any number of segments):
Since
fastify-otel/index.jsis already a vendored file that we maintain (and diverge fromupstream where needed), this change is straightforward.
Replace with a Better Alternative
5.
recast+@babel/parser—@sentry/sveltekitTransitive dep count: ~8 packages combined
recastbrings:ast-types,source-map,tslib@babel/parserbrings:@babel/types,@babel/helper-string-parser,@babel/helper-validator-identifier,to-fast-propertiesWhere they are used:
packages/sveltekit/src/vite/autoInstrument.tsandrecastTypescriptParser.ts. The pluginreads SvelteKit
+page.ts/+layout.server.tsfiles at build time, parses them as TypeScriptASTs, and checks whether a top-level
export const loadorexport function loaddeclarationexists. It does not actually mutate the AST — if
loadis found, it discards the entiremodule and returns a freshly generated wrapper string. Because the AST is only read (never
rewritten in place), recast's write-preserving round-trip feature is never exercised, and
@babel/parseris only used as the parse step.Better alternative:
acorn+acorn-typescriptacornis the parser used by Node.js itself (0 transitive deps).acorn-typescriptaddsTypeScript syntax support as an acorn plugin (0 transitive deps). The existing check only needs
to walk top-level
ExportNamedDeclarationnodes, which maps directly onto acorn's AST output.magic-string(already a dependency of@sentry/sveltekit) continues to handle theappend/prependoperations ininjectGlobalValues.tsunchanged.Dep reduction: Removes
recast,ast-types,source-map,@babel/parser,@babel/types,@babel/helper-string-parser,@babel/helper-validator-identifier,to-fast-properties(~8packages). Adds
acorn+acorn-typescript(0 transitive deps each).