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-referent

v1.2.1

Published

Automated TypeScript project references for monorepos, not just between packages, but inside them.

Readme

ts-referent

Automated TypeScript project references for monorepos, not just between packages, but inside them.

ts-referent splits every package into isolated slices called kinds such as source, tests, cypress, storybook, CJS, or ESM, and generates the tsconfig files needed to enforce boundaries between them. Source code cannot accidentally import test utilities. Jest types do not leak into production. Cypress globals do not clash with Jest globals. These boundaries are real because TypeScript enforces them at compile time.

Quick start

Install the package:

npm install --save-dev ts-referent
# or: yarn add --dev ts-referent
# or: pnpm add --save-dev ts-referent

Create a root tsconfig.referent.ts or tsconfig.referent.js:

import { configure } from 'ts-referent';

export default configure({
  baseConfig: 'tsconfig.json',
  kinds: {
    base: {
      include: ['**/*'],
      exclude: ['**/*.spec.*'],
    },
    tests: {
      include: ['**/*.spec.*'],
      references: ['base'],
      types: ['jest'],
    },
  },
});

Make sure your base tsconfig.json disables automatic type discovery so each kind controls its own type environment:

{
  "compilerOptions": {
    "types": []
  }
}

Generate package-level tsconfig files:

npx ts-referent build

For full-workspace type-checking, generate a separate solution-style config and build that instead of your root tsconfig.json:

npx ts-referent glossary tsconfig.packages.json
tsc -b tsconfig.packages.json

That is the core workflow. Define kinds once, generate configs, and let TypeScript enforce the boundaries.

What you get

With the minimal setup above:

  • production code cannot import test-only code
  • test-only globals do not leak into source files
  • each package gets multiple generated tsconfig files derived from one shared configuration
  • project references stay aligned with your workspace packages and local boundaries

Core commands

ts-referent build

Reads your kinds configuration, scans every package in the monorepo, and generates tsconfig files for each one.

npx ts-referent build

This should usually run on every postinstall so generated configs stay in sync with package.json dependencies:

{
  "scripts": {
    "postinstall": "ts-referent build"
  }
}

Yarn Classic note: postinstall has a known issue in Yarn 1.x. For Yarn 2+, use the recommended plugin from the project documentation.

ts-referent glossary <filename>

Generates a solution-style tsconfig that references all packages. Use it for global type-checking, CI, or any place where you want tsc -b to cover the whole workspace.

npx ts-referent glossary tsconfig.packages.json
tsc -b tsconfig.packages.json

This file does not need to be committed. Generate it on demand.

Available filters:

  • --filter-by-name <glob> includes only packages matching the name pattern
  • --filter-by-folder <glob> includes only packages in matching directories

ts-referent paths <filename>

Generates paths aliases for all local packages. Extend your base config from this file to improve editor auto-imports and local package resolution.

npx ts-referent paths tsconfig.paths.json

Available options:

  • --extends <config> specifies a config to extend from

TypeScript 5 and newer support multiple extends, so this option is often unnecessary there.

entrypointResolver is only required if you use this command.

Setup requirements

These are easy to miss, but they matter.

types: [] in the base tsconfig

This disables automatic @types discovery, which is necessary if kinds are meant to control type visibility per slice.

TypeScript 6.0 makes this the default, but setting it explicitly keeps the behavior clear.

Keep glossary in a separate file

Do not put glossary references into your root tsconfig.json. Use tsconfig.packages.json or a similar separate file.

Including workspace-wide references in the root config can cause WebStorm's TypeScript server to hang.

Include all source in the root tsconfig

Nested generated configs will narrow things further, but exposing all source to TypeScript at the root improves cross-package auto-imports.

Design principles

Generated tsconfig files are build artifacts

ts-referent generates tsconfig files at every level: in .referent, in package roots, and anywhere your kinds require them. Treat all of these files as generated output.

They should be gitignored.

.referent
# and any per-package tsconfigs generated by ts-referent

Generated configs may reference directories that do not exist yet. They may contain paths that only make sense after install. Committing them creates stale diffs, stale pointers, and confusion about what the source of truth actually is.

The intended workflow is simple: regenerate them on every postinstall.

You configure generation, not the generated files

Generated tsconfig files are disposable. Do not edit them by hand.

To change how a package's TypeScript environment behaves:

  1. Change tsconfig.referent.js or tsconfig.referent.ts
  2. Change package.json

ts-referent reads each package's package.json and passes it into your configuration. That means you can drive kinds, compiler options, type definitions, and dependency directions from package metadata.

package.json is the input, generated tsconfig files are the output, and tsconfig.referent is the transformation between them.

export default configure({
  kinds: (inheritedKinds, currentPackage) => ({
    base: {
      types: currentPackage.packageJson.needsNode ? ['node'] : [],
      externals: currentPackage.packageJson.externals,
    },
    tests: {
      include: ['**/*.spec.*'],
      references: ['base'],
      types: ['jest'],
    },
  }),
});

Kinds

A kind is a named slice of a package, defined by include and exclude glob patterns. Each kind becomes a separate tsconfig with its own type environment and dependency rules.

Each kind can define:

  • include and exclude to decide which files belong to the slice
  • types to control which @types/* packages are visible
  • references to define which other kinds this slice may import from
  • compilerOptions for per-kind compiler option overrides
  • externals for project references outside the normal dependency graph
  • outputDirectory and focusOnDirectory for publishing scenarios

References are directional. tests -> base does not imply base -> tests.

Configuration

Configuration lives in tsconfig.referent.js or tsconfig.referent.ts. Place one at any directory level and it will affect all packages below that directory.

A configuration file exports baseConfig, kinds, and optionally entrypointResolver and useBaseUrl.

Basic root configuration

CommonJS:

/** @type {import('ts-referent').ConfigurationFile} */
module.exports = {
  baseConfig: 'tsconfig.json',
  kinds: {
    base: {
      include: ['**/*'],
      exclude: ['**/*.spec.*'],
    },
    tests: {
      include: ['**/*.spec.*'],
      references: ['base'],
      types: ['jest'],
    },
  },
};

TypeScript with the typed helper:

import { configure } from 'ts-referent';

export default configure({
  baseConfig: require.resolve('tsconfig.json'),
  kinds: {
    base: {
      include: ['**/*'],
      exclude: ['**/*.spec.*'],
    },
    tests: {
      include: ['**/*.spec.*'],
      references: ['base'],
      types: ['jest'],
    },
  },
});

TypeScript 6+

Set useBaseUrl: false in your configuration.

baseUrl is deprecated in TypeScript 6.0 and will be removed in TypeScript 7.0. Since TypeScript 4.1, baseUrl is no longer required for paths to work. useBaseUrl defaults to true for backward compatibility.

module.exports = {
  useBaseUrl: false,
  baseConfig: 'tsconfig.json',
  kinds: {
    /* ... */
  },
};

Using ESM or TypeScript config files

Run ts-referent through a loader:

node -r tsm ts-referent build
# or with yarn
node -r tsm $(yarn bin ts-referent) build

Example config:

import type { EntrypointResolver, Kinds } from 'ts-referent';

export const baseConfig = 'tsconfig.json';
export const entrypointResolver: EntrypointResolver = (packageJSON, dir) => [];

export const kinds: Kinds = {
  base: {
    include: ['**/*'],
    exclude: ['**/*.spec.*'],
  },
  tests: {
    include: ['**/*.spec.*'],
    references: ['base'],
    types: ['jest'],
  },
};

Dynamic kinds

kinds can be a function. It receives the inherited kinds from parent configuration and the current package, so you can vary behavior per package based on package.json or other metadata.

export default configure({
  kinds: ({ base, ...rest }, currentPackage) => ({
    ...rest,
    base: {
      ...base,
      externals: currentPackage.packageJson.externals,
      types: [...(base.types || []), 'node'],
      exclude: ['**/*.spec.*'],
    },
    tests: {
      include: ['**/*.spec.*'],
      references: ['base'],
    },
  }),
});

Alter inherited kinds

Use alter in nested tsconfig.referent.ts files when you want to modify inherited kinds without redefining everything.

import { alter } from 'ts-referent';

export default alter((currentPackage) => ({
  base: {
    externals: currentPackage.packageJson.externals,
    types: ['node'],
    exclude: ['**/*.spec.*'],
  },
  tests: {
    include: ['**/*.spec.*'],
    references: ['base'],
  },
}));

Configuration returned from alter is merged with inherited configuration.

To remove a kind, set it to null, set enable: false, or use disableUnmatchedKinds:

export default alter(
  (currentPackage) => ({
    base: {
      /* ... */
    },
    tests: {},
  }),
  { disableUnmatchedKinds: true }
);

Type augmentation

Extend PackageJSON if you want custom fields to be visible inside ts-referent configuration functions:

declare module 'ts-referent' {
  interface PackageJSON {
    externals?: ReadonlyArray<string>;
    needsNode?: boolean;
  }
}

export default alter((currentPackage) => ({
  base: {
    externals: currentPackage.packageJson.externals,
    types: currentPackage.packageJson.needsNode ? ['node'] : [],
  },
}));

Note: project references can affect module augmentation because of how .d.ts files are generated from source files. If augmentation stops working, you may need to author .d.ts files manually.

entrypointResolver

This is only required for ts-referent paths. It maps package exports to path aliases.

const pickExport = (entry: string | Record<string, string>) => {
  if (typeof entry === 'string') return entry;
  return entry['import'] || entry['require'];
};

export default configure({
  baseConfig: require.resolve('tsconfig.json'),
  entrypointResolver: (packageJSON, dir) => {
    if (!packageJSON.exports) return [];

    return Object.entries(packageJSON.exports).map(([relativeName, pointsTo]) => {
      const name = relativeName.substring(2);
      return [name ? `/${name}` : '', pickExport(pointsTo)];
    });
  },
  kinds: {
    /* ... */
  },
});

For anything beyond a flat export map, use resolve.exports.

Isolation

By default, a package's generated tsconfig references all of its kinds. Any other package referencing it can therefore see not only its source kind, but also tests, storybook, and everything else.

Two features tighten this behavior.

isolatedInDirectory

This is a per-kind setting. It places a kind's tsconfig inside a nested directory such as cypress/, __tests__/, or examples/, making it truly local.

The kind is created only if the directory exists.

Other kinds in the same package can still access it through references. Referencing an isolated directory from the workspace level is possible through relationMapper, but should be done carefully.

isolatedMode

This is a global flag in the root tsconfig.referent.js.

When enabled, each package produces two configs:

  • tsconfig.json for the IDE
  • tsconfig.public.json for external references

Use the internal per-kind flag to exclude kinds from the public config:

  • internal: true keeps a kind visible only inside the package
  • kinds created with isolatedInDirectory are private by default, but can be made public with internal: false

Publishing packages

Use separate kinds when you need separate CJS and ESM output:

export default configure({
  kinds: {
    cjs: {
      include: ['**/*'],
      exclude: ['**/*.spec.*'],
      compilerOptions: {
        target: 'es5',
        module: 'commonjs',
        verbatimModuleSyntax: false,
      },
      outputDirectory: 'dist/cjs',
      focusOnDirectory: 'src',
    },
    esm: {
      include: ['**/*'],
      exclude: ['**/*.spec.*'],
      outputDirectory: 'dist/esm',
      focusOnDirectory: 'src',
    },
  },
});

In monorepos where only some packages are published, put those packages under a shared directory and use alter:

import { alter } from 'ts-referent';

export default alter((_, kinds) => ({
  base: {
    outputDirectory: 'dist/esm',
    focusOnDirectory: 'src',
  },
  'base-cjs': {
    expectExtension: true,
    ...kinds['base'],
    outputDirectory: 'dist/cjs',
    focusOnDirectory: 'src',
    compilerOptions: {
      target: 'es5',
      module: 'commonjs',
    },
  },
}));

IDE notes

VS Code

VS Code handles project references out of the box.

WebStorm

Enable Recompile on changes in TypeScript settings.

With project references, incremental recompilation is typically fast. Since TypeScript 3.7, build-free editing works unless disableSourceOfProjectReferenceRedirect is enabled. If you do need compiled output, tsc -b --watch works well for smaller projects.

Why this exists

TypeScript project references let you split a codebase into sub-projects with explicit dependency edges. In a monorepo, the obvious use is linking packages to each other.

But there is a second problem inside each package. A single package usually contains source code, unit tests, integration tests, storybook stories, benchmarks, and other files that all need different type environments. Putting them all in one tsconfig creates ghostly type definitions where describe and it are globally available in production code, or where Cypress globals collide with Jest globals.

ts-referent solves this by generating multiple tsconfig files per package, with directional dependency rules between them. Tests can import source. Source cannot import tests. Each slice gets exactly the type definitions it needs.

How it compares

Most tools in this space sync inter-package dependencies into tsconfig.references. ts-referent also handles the harder intra-package case by generating multiple tsconfig files per package with isolated type environments and directional boundaries.

Monorepo orchestrators

  • Nx syncs project references and scaffolds a fixed split such as tsconfig.lib.json and tsconfig.spec.json. That covers the common case inside the Nx ecosystem, but not arbitrary slices with custom dependency directions.
  • moonrepo has strong built-in support for project references and recommends ts-referent as a complementary tool for intra-package slicing.
  • Turborepo generally recommends consuming raw .ts source directly instead of leaning on project references. That avoids some configuration overhead, but it also gives up build-boundary enforcement and is not a fit for published packages.
  • Rush and Lerna do not provide TypeScript-aware project reference management.

Standalone reference-sync tools

Tools such as update-ts-references, workspaces-to-typescript-project-references, and set-project-references operate at the package level only. They manage one tsconfig per package and sync references from workspace dependencies.

ts-referent works at the project-slice level: multiple tsconfig files per package, derived from shared configuration that can react to each package's package.json.

See also

  • One Thing Nobody Explained To You About TypeScript
  • TypeScript Project References handbook
  • eslint-plugin-relations for complementary import restriction rules
  • update-ts-references for inter-package reference syncing
  • moonrepo's guide to TypeScript project references

License

MIT