npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ts-prefix-internals

v0.2.4

Published

TypeScript symbol prefixer for aggressive minification — prefix internal symbols with _ so terser/esbuild can mangle them via mangleProps: /^_/

Readme

ts-prefix-internals

CI

A TypeScript CLI tool that prefixes internal symbols with _ so that terser or esbuild can aggressively mangle them via mangleProps: /^_/.

The Problem

JavaScript minifiers like terser and esbuild can mangle local variable names, but they cannot safely mangle property and method names. They have no way to know which names are internal implementation details versus part of a public API or DOM/library interface. Mangling the wrong property name breaks your code at runtime.

This means class properties, method names, and other member identifiers ship unminified in your production bundles, often accounting for a significant portion of the remaining code size after standard minification.

The Solution

TypeScript's compiler API does know which symbols are internal. It has full type information, knows about exports, class visibility modifiers, and the entire dependency graph.

ts-prefix-internals uses this information to:

  1. Discover your public API surface from barrel export entry points
  2. Classify every symbol in your project as public or internal
  3. Prefix internal symbols with _ using the TypeScript Language Service for safe, cross-file renaming
  4. Validate the output compiles without errors

After prefixing, you configure your minifier to mangle everything matching /^_/:

// terser
{
  mangle: {
    properties: { regex: /^_/ }
  }
}

// esbuild
{
  mangleProps: /^_/
}

What Gets Prefixed

Internal (will be prefixed):

  • Classes, functions, and variables not exported from your entry points
  • Private members of exported classes
  • All members of non-exported classes
  • Internal interfaces, type aliases, and enums

Public API (will NOT be prefixed):

  • Symbols exported from entry point files
  • Public/protected members of exported classes
  • Members of exported interfaces and enums
  • Types referenced in public API signatures (followed recursively)
  • Anything from node_modules or .d.ts files
  • Symbols already starting with _

Install

npm install -D ts-prefix-internals

Usage

CLI

# Preview what would be renamed
npx ts-prefix-internals -p tsconfig.json -e src/index.ts -o .prefixed --dry-run

# Rename and write output
npx ts-prefix-internals -p tsconfig.json -e src/index.ts -o .prefixed

# Multiple entry points
npx ts-prefix-internals -p tsconfig.json -e src/index.ts -e src/server.ts -o .prefixed

Options

-p, --project <path>      Path to tsconfig.json (required)
-e, --entry <file>        Public API entry point file(s), repeatable (required)
-o, --outDir <dir>        Output directory for rewritten files (required)
    --prefix <string>     Prefix string (default: "_")
    --dry-run             Report what would be renamed without writing files
    --verbose             Print every rename decision with reasoning
    --skip-validation     Skip post-rename type-check
    --force               Continue despite dynamic-access errors (exit 0)
-h, --help                Show help

Programmatic API

import { prefixInternals } from 'ts-prefix-internals';

const result = await prefixInternals({
  projectPath: 'tsconfig.json',
  entryPoints: ['src/index.ts'],
  outDir: '.prefixed',
  prefix: '_',
  dryRun: false,
  verbose: false,
  skipValidation: false,
});

console.log(`Prefixed ${result.willPrefix.length} symbols`);
console.log(`Kept ${result.willNotPrefix.length} public API symbols`);

if (result.validationErrors) {
  console.error('Output has type errors:', result.validationErrors);
}

Example

Given a project with a barrel export:

// src/index.ts
export { Processor } from './engine';
export { Coord, CoordKind } from './types';

And an internal class not exported from the barrel:

// src/graph.ts
export class LinkMap {
  private forward: Map<string, Set<string>>;
  connect(a: string, b: string): void { /* ... */ }
}

Running the tool produces:

WILL PREFIX (internal):
  LinkMap                  class    graph.ts:1     -> _LinkMap
  LinkMap.forward          property graph.ts:2     -> _forward
  LinkMap.connect          method   graph.ts:4     -> _connect
  Processor.links          property engine.ts:5    -> _links
  Processor.pending        property engine.ts:6    -> _pending

WILL NOT PREFIX (public API):
  Processor                class    engine.ts:4    (exported)
  Processor.setEntry       method   engine.ts:15   (public member)
  Coord                    interface types.ts:1    (exported)
  Coord.ns                 property types.ts:2     (interface member)

The output directory contains valid TypeScript where all internal symbols are prefixed, ready for aggressive minification.

How It Works

  1. API Surface Discovery (api-surface.ts) -- Starting from entry point barrel exports, recursively discovers every public symbol. Resolves alias chains (export { Foo } from './foo'), walks type signatures to find referenced types, includes public/protected class members, interface members, and enum members.

  2. Symbol Classification (classifier.ts) -- Walks every source file and classifies each symbol. Uses the public API set to determine what's internal. Handles private class members, constructor parameter properties, and generates warnings for dynamic property access.

  3. Renaming (renamer.ts) -- Uses the TypeScript Language Service findRenameLocations API for safe cross-file renaming. Collects all edits first, deduplicates, sorts in reverse order, and applies bottom-to-top to avoid position shifts.

  4. Validation -- Compiles the output with tsc --noEmit to verify the renaming was safe.

Build Pipeline Integration

A typical build pipeline using this tool:

# 1. Prefix internal symbols
npx ts-prefix-internals -p tsconfig.json -e src/index.ts -o .prefixed

# 2. Compile the prefixed source
cd .prefixed && tsc

# 3. Bundle and mangle with terser/esbuild
esbuild .prefixed/dist/index.js --bundle --minify --mangle-props=_

Or as package.json scripts:

{
  "scripts": {
    "prefix": "ts-prefix-internals -p tsconfig.json -e src/index.ts -o .prefixed",
    "build": "npm run prefix && cd .prefixed && tsc && esbuild dist/index.js --bundle --minify --mangle-props=_"
  }
}

Releasing

# patch release (default)
npm run release

# minor/major/prerelease
npm run release -- minor

Release script behavior:

  1. Verifies you're on main and the working tree is clean
  2. Runs tests
  3. Bumps version with npm version
  4. Pushes branch + tag

Publishing behavior:

  • Tag pushes matching v* trigger .github/workflows/publish.yml
  • Workflow builds/tests, verifies tag version matches package.json, publishes to npm, and creates a GitHub Release
  • Prerelease versions (for example 1.2.0-beta.1) publish with npm dist-tag next

One-time setup:

  1. In npm, configure a Trusted Publisher for this GitHub repo and workflow file
  2. In GitHub, create environment npm-release and optionally require reviewers / restrict tags
  3. Protect release tags (for example v*) with a GitHub ruleset so only maintainers can create them

Safety

  • Uses the TypeScript Language Service for renaming (not regex/text replacement)
  • Validates output compiles after renaming
  • Never modifies symbols from node_modules or .d.ts files
  • Skips decorated symbols (decorator name reflection would break)
  • Detects dynamic property access with three-tier diagnostics:
    • Error: bracket access with a string literal type that matches a prefixed property (provably broken)
    • Warn: bracket access with a broad string or any type (may break)
    • Silent: array/tuple indexing (safe, suppressed)
  • Skips symbols already starting with _

Requirements

  • Node.js >= 18
  • TypeScript >= 5.0 (as a project dependency)

License

MIT