# 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:

```bash
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`:

```ts
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:

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

Generate package-level `tsconfig` files:

```bash
npx ts-referent build
```

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

```bash
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.

```bash
npx ts-referent build
```

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

```json
{
  "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.

```bash
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.

```bash
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.

```gitignore
.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.

```ts
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:

```js
/** @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:

```ts
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.

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

### Using ESM or TypeScript config files

Run `ts-referent` through a loader:

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

Example config:

```ts
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.

```ts
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.

```ts
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`:

```ts
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:

```ts
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.

```ts
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:

```ts
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`:

```ts
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
