Skip to content

Replace SDK package dependencies #19447

@Lms24

Description

@Lms24

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 SKD 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions