# llms-full (private-aware)
> Built from GitHub files and website pages. Large files may be truncated.

--- docs/overview.md ---
---
id: overview
title: Overview
---

TanStack Config is a collection of tools we currently use between our projects to simplify configuration.

## Required Pre-Requisites

The following tools are required to use these packages:

- [Node.js v18.17+](https://nodejs.org/en/download/current/)
- [Git CLI](https://git-scm.com/downloads)
- [GitHub CLI](https://cli.github.com/) (pre-installed on GitHub Actions)
- [pnpm v8+](https://pnpm.io/)

> pnpm is the only supported package manager for TanStack Config.

## Utilities

- [ESLint](./eslint.md)
- [Publish](./publish.md)
- [Vite](./vite.md)

## Conventions

- [CI/CD](./ci-cd.md)
- [Dependencies](./dependencies.md)
- [Package Structure](./package-structure.md)


## Links discovered
- [Node.js v18.17+](https://nodejs.org/en/download/current/)
- [Git CLI](https://git-scm.com/downloads)
- [GitHub CLI](https://cli.github.com/)
- [pnpm v8+](https://pnpm.io/)
- [ESLint](https://github.com/TanStack/config/blob/main/docs/eslint.md)
- [Publish](https://github.com/TanStack/config/blob/main/docs/publish.md)
- [Vite](https://github.com/TanStack/config/blob/main/docs/vite.md)
- [CI/CD](https://github.com/TanStack/config/blob/main/docs/ci-cd.md)
- [Dependencies](https://github.com/TanStack/config/blob/main/docs/dependencies.md)
- [Package Structure](https://github.com/TanStack/config/blob/main/docs/package-structure.md)

--- docs/ci-cd.md ---
---
id: ci-cd
title: CI/CD
---

## GitHub Workflows

- `pr.yml`:
    - Runs tests for all pull requests
    - Runs `nx affected`, which only executes tasks with invalidated cache
    - Also uses `pkg-pr-new` to publish package previews and create links to our examples
- `release.yml`:
    - Runs tests for code merged into release branches
    - Runs `nx run-many`, which executes all tasks and ensures the outputs are present (necessary for publishing builds)
    - Uses [Changesets](https://github.com/changesets/changesets) to handle versioning and publishing

## Nx

The TanStack projects use Nx to enable rapid execution of our tests and builds. Tasks are parallelised and cached both locally and in CI. While Nx has an extensive plugin system, we only utilise Nx as an NPM script runner.

### Config Files

- `./nx.json`: Main config file, which defines task dependencies, inputs, and outputs
- `./package.json`: Need to manually specify root-level scripts (e.g. `test:format`)
- `./**/package.json`: Package-level scripts (e.g. `build`) are automatically detected

### Nx Agents

- Nx allows you to distribute your tasks across multiple CI machines, increasing the number of jobs that can be run in parallel
- Please note that this does incur quite a significant startup delay


## Links discovered
- [Changesets](https://github.com/changesets/changesets)

--- docs/dependencies.md ---
---
id: dependencies
title: Dependencies
---

We use 3 separate tools to help manage our dependencies and prevent us from unnecessarily bloating the `node_modules` directory.

### Sherif

- Sherif ensures that all references to a dependency throughout the monorepo are on the same version
- This helps avoid pnpm resolution issues, such as type conflicts from having 2+ incompatible versions of the same dependency installed

### Knip

- Knip is able to detect unused dependencies within `package.json` files
- This leads to fewer packages getting installed unnecessarily by developers

### Renovate

- Renovate is a bot which runs on GitHub to scan for outdated or insecure dependencies
- This reduces the burden on maintainers by automatically submitting PRs

--- docs/eslint.md ---
---
id: eslint
title: ESLint
---

## Purpose

This package unifies the shared ESLint config used across all TanStack projects. It is designed to be framework-agnostic, and does not include any framework-specific plugins.

## Installation

To install the package, run the following command:

```bash
pnpm add -D @tanstack/eslint-config
```

## Setup

### package.json

- Make sure you have ESLint v9+ installed

### eslint.config.js

```js
import { tanstackConfig } from '@tanstack/eslint-config'

export default [
  ...tanstackConfig,
  {
    // Custom rules go here
  },
]
```

## Plugins

- [@eslint/js](https://github.com/eslint/eslint) - The core ESLint rules
- [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) - Enables TypeScript support
- [eslint-plugin-import-x](https://github.com/un-ts/eslint-plugin-import-x) - Lints imports and exports
- [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n) - Useful rules for Node.js

## Rules

You can inspect the enabled rules by running `pnpm dlx @eslint/config-inspector`, or by browsing the source [here](https://github.com/TanStack/config/tree/main/packages/eslint-config). Each rule has a comment explaining why it is included in the shared config.


## Links discovered
- [@eslint/js](https://github.com/eslint/eslint)
- [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint)
- [eslint-plugin-import-x](https://github.com/un-ts/eslint-plugin-import-x)
- [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n)
- [here](https://github.com/TanStack/config/tree/main/packages/eslint-config)

--- docs/package-structure.md ---
---
id: package-structure
title: Package Structure
---

The following structure ensures packages work optimally with our monorepo/Nx workflow.

### `./package.json`

- All TanStack projects have `"type": "module"` to set the default resolution of `.js` files to ESM; this does not have any impact on building for CJS
- It is also essential to have an `"exports"` field
- For legacy reasons, you should also include the `"main"`, `"module"`, and `"types"` fields
- All packages have the following scripts which are cached by Nx: `"test:eslint"`, `"test:types"`, `"test:lib"`, `"build"`, `"test:build"`

### `./tsconfig.json`

- Extends the root-level tsconfig (e.g. `"extends": "../../tsconfig.json"`)
- Add any framework-specific options and included files here

### `./vite.config.ts`

- Includes config for Vitest, and for Vite if [@tanstack/vite-config](./vite.md) is used

### `./src`

- This folder should only include code which gets built and shipped to users
- Tests should not be placed in this folder, as they bloat the shipped code, and can unintentionally invalidate the Nx cache

### `./tests`

- This folder should include all test files
- It should also include any test setup files required by that framework


## Links discovered
- [@tanstack/vite-config](https://github.com/TanStack/config/blob/main/docs/vite.md)

--- docs/publish.md ---
---
id: publish
title: Publish
---

## Installation

To install the package, run the following command:

```bash
pnpm add -D @tanstack/publish-config
```


## Usage

To use the TanStack Config programmatically, you can import the `publish` function:

```ts
import { publish } from '@tanstack/publish-config'

publish({
  branchConfigs: configOpts.branchConfigs,
  packages: configOpts.packages,
  rootDir: configOpts.rootDir,
  branch: process.env.BRANCH,
  tag: process.env.TAG,
  ghToken: process.env.GH_TOKEN,
})
  .then(() => {
    console.log('Successfully published packages!')
  })
  .catch(console.error)
```

> The programmatic usage is only available for ESM packages. To support this, you have to have:
>
> ```json
> {
>   "type": "module"
> }
> ```
>
> in your `package.json` file and use `import` instead of `require`.


--- docs/vite.md ---
---
id: vite
title: Vite
---

The Vite build setup is the culmination of several attempts to dual publish ESM and CJS for TanStack projects, while preserving compatibility with all Typescript module resolution options.

## Do I Need This?

ES Modules (ESM) is the standard for writing JavaScript modules. However, due to the historical dependency on CommonJS (CJS), many ecosystem tools and projects were initially incompatible with ESM. It is becoming exceedingly rare for this to be the case, and I would urge you to consider whether it is necessary to distribute CJS code at all. Sindre Sorhus has a good summary on this issue [here](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).

## Installation

To install the package, run the following command:

```bash
pnpm add -D @tanstack/vite-config
```

## Setup

The build config is quite opinionated, as it is designed to work with our internal libraries. If you follow the below instructions, it _may_ work for your library too!

### package.json

- Ensure `"type": "module"` is set.
- Ensure you have [Vite](https://www.npmjs.com/package/vite) installed. Installing [Publint](https://www.npmjs.com/package/publint) is also recommended.
- Change your build script to `"build": "vite build && publint --strict"`
- Ensure you have an `"exports"` field. We use this, but you might have different requirements:

```json
{
  "exports": {
    ".": {
      "import": {
        "types": "./dist/esm/index.d.ts",
        "default": "./dist/esm/index.js"
      },
      "require": {
        "types": "./dist/cjs/index.d.cts",
        "default": "./dist/cjs/index.cjs"
      }
    },
    "./package.json": "./package.json"
  }
}
```

### tsconfig.json

- Ensure your `"include"` field includes `"vite.config.ts"`.
- Set `"moduleResolution"` to `"bundler"`.

### vite.config.ts

- Import `mergeConfig` and `tanstackViteConfig`.
- Merge your custom config first, followed by `tanstackViteConfig`.
- Please avoid modifying `build` in your custom config.
- See an example below:

```ts
import { defineConfig, mergeConfig } from 'vite'
import { tanstackViteConfig } from '@tanstack/vite-config'

const config = defineConfig({
  // Framework plugins, vitest config, etc.
})

export default mergeConfig(
  config,
  tanstackViteConfig({
    entry: './src/index.ts',
    srcDir: './src',
  }),
)
```

## Frameworks

While this config _will_ work with most frameworks with a Vite adapter, it doesn't mean you _should_ use it for all frameworks, as many have their own build tools which are optimised for their ecosystem. When a framework-specific build tool exists, this should be preferred.

| Framework | Recommendation                                                                                     |
| --------- | -------------------------------------------------------------------------------------------------- |
| Angular   | [ng-packagr](https://www.npmjs.com/package/ng-packagr) (official tool)                             |
| React     | [@tanstack/vite-config](https://www.npmjs.com/package/@tanstack/vite-config) (only if you need dual ESM/CJS) |
| Solid     | [tsc](https://www.npmjs.com/package/typescript) (preserves JSX, necessary for SSR)                 |
| Svelte    | [@sveltejs/package](https://www.npmjs.com/package/@sveltejs/package) (official tool)               |
| Vue       | [@tanstack/vite-config](https://www.npmjs.com/package/@tanstack/vite-config) (only if you need dual ESM/CJS) |


## Links discovered
- [here](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
- [Vite](https://www.npmjs.com/package/vite)
- [Publint](https://www.npmjs.com/package/publint)
- [ng-packagr](https://www.npmjs.com/package/ng-packagr)
- [@tanstack/vite-config](https://www.npmjs.com/package/@tanstack/vite-config)
- [tsc](https://www.npmjs.com/package/typescript)
- [@sveltejs/package](https://www.npmjs.com/package/@sveltejs/package)

--- .github/comment-on-release/README.md ---
# Comment on Release Action

A reusable GitHub Action that automatically comments on PRs and linked issues when they are included in a release.

## What It Does

When packages are published via Changesets:

1. Parses each published package's CHANGELOG to find PR numbers in the latest version
2. Groups PRs by number (handling cases where one PR affects multiple packages)
3. Posts a comment on each PR with release info and CHANGELOG links
4. Finds issues that each PR closes/fixes using GitHub's GraphQL API
5. Posts comments on linked issues notifying them of the release

## Example Comments

### On a PR:

```
🎉 This PR has been released!

- [@tanstack/query-core@5.0.0](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md#500)
- [@tanstack/react-query@5.0.0](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md#500)

Thank you for your contribution!
```

### On a linked issue:

```
🎉 The PR fixing this issue (#123) has been released!

- [@tanstack/query-core@5.0.0](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md#500)

Thank you for reporting!
```

## Usage

Add this step to your `.github/workflows/release.yml` file after the `changesets/action` step:

```yaml
- name: Run Changesets (version or publish)
  id: changesets
  uses: changesets/action@v1.5.3
  with:
    version: pnpm run changeset:version
    publish: pnpm run changeset:publish
    commit: 'ci: Version Packages'
    title: 'ci: Version Packages'
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Comment on PRs about release
  if: steps.changesets.outputs.published == 'true'
  uses: tanstack/config/.github/comment-on-release@main
  with:
    published-packages: ${{ steps.changesets.outputs.publishedPackages }}
```

## Requirements

- Must be using [Changesets](https://github.com/changesets/changesets) for releases
- CHANGELOGs must include PR links in the format: `[#123](https://github.com/org/repo/pull/123)`
- Requires `pull-requests: write` and `issues: write` permissions in the workflow
- The `gh` CLI must be available (automatically available in GitHub Actions)

## Inputs

| Input                | Required | Description                                                        |
| -------------------- | -------- | ------------------------------------------------------------------ |
| `published-packages` | Yes      | JSON string of published packages from `changesets/action` outputs |

## How It Works

The action:

1. Receives the list of published packages from the Changesets action
2. For each package, reads its CHANGELOG at `packages/{package-name}/CHANGELOG.md`
3. Extracts PR numbers from the latest version section using regex
4. Groups all PRs and tracks which packages they contributed to
5. Posts a single comment per PR listing all packages it was released in
6. For each PR, queries GitHub's GraphQL API to find linked issues (via `closes #N` or `fixes #N` keywords)
7. Groups issues and tracks which PRs fixed them
8. Posts comments on linked issues notifying them of the release
9. Checks for duplicate comments to avoid spamming
10. Uses the `gh` CLI to post comments via the GitHub API

## Troubleshooting

**No comments are posted:**

- Verify your CHANGELOGs have PR links in the correct format
- Check that `steps.changesets.outputs.published` is `true`
- Ensure the workflow has `pull-requests: write` and `issues: write` permissions

**Script fails to find CHANGELOGs:**

- The script expects packages at `packages/{package-name}/CHANGELOG.md`
- Package name should match after removing the scope (e.g., `@tanstack/query-core` → `query-core`)

**Issues aren't being commented on:**

- Verify that PRs use GitHub's closing keywords (`closes #N`, `fixes #N`, `resolves #N`, etc.) in the PR description
- Check that the linked issues exist and are accessible
- Ensure the `issues: write` permission is granted in the workflow


## Links discovered
- [@tanstack/query-core@5.0.0](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md#500)
- [@tanstack/react-query@5.0.0](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md#500)
- [Changesets](https://github.com/changesets/changesets)
- [#123](https://github.com/org/repo/pull/123)

--- CONTRIBUTING.md ---
# Contributing

## Questions

If you have questions about implementation details, help or support, then please use our dedicated community forum at [GitHub Discussions](https://github.com/TanStack/config/discussions) **PLEASE NOTE:** If you choose to instead open an issue for your question, your issue will be immediately closed and redirected to the forum.

## Reporting Issues

If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/config/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/config/discussions)

## Suggesting new features

If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented.

## Development

If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started:

- Fork this repository.
- Install dependencies

  ```bash
  pnpm install
  ```

  - We use [pnpm](https://pnpm.io/) v9 for package management (run in case of pnpm-related issues).

    ```bash
    corepack enable && corepack prepare
    ```

  - We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc`

    ```bash
    nvm use
    ```

- Build all packages.

  ```bash
  pnpm build:all
  ```

- Run development server.

  ```bash
  pnpm run watch
  ```

- Implement your changes and tests to files in the `src/` directory and corresponding test files.
- Document your changes in the appropriate doc page.
- Git stage your required changes and commit (see below commit guidelines).
- Submit PR for review.

### Editing the docs locally and previewing the changes

The documentations for all the TanStack projects are hosted on [tanstack.com](https://tanstack.com), which is a TanStack Start application (https://github.com/TanStack/tanstack.com). You need to run this app locally to preview your changes in the `TanStack/config` docs.

> [!NOTE]
> The website fetches the doc pages from GitHub in production, and searches for them at `../config/docs` in development. Your local clone of `TanStack/config` needs to be in the same directory as the local clone of `TansStack/tanstack.com`.

You can follow these steps to set up the docs for local development:

1. Make a new directory called `tanstack`.

```sh
mkdir tanstack
```

2. Enter that directory and clone the [`TanStack/config`](https://github.com/TanStack/config) and [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com) repos.

```sh
cd tanstack
git clone git@github.com:TanStack/config.git
# We probably don't need all the branches and commit history
# from the `tanstack.com` repo, so let's just create a shallow
# clone of the latest version of the `main` branch.
# Read more about shallow clones here:
# https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-shallow-clones
git clone git@github.com:TanStack/tanstack.com.git --depth=1 --single-branch --branch=main
```

> [!NOTE]
> Your `tanstack` directory should look like this:
>
> ```
> tanstack/
>    |
>    +-- config/ (<-- this directory cannot be called anything else!)
>    |
>    +-- tanstack.com/
> ```

3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode:

```sh
cd tanstack.com
pnpm i
# The app will run on https://localhost:3000 by default
pnpm dev
```

4. Now you can visit http://localhost:3000/config/latest/docs/overview in the browser and see the changes you make in `TanStack/config/docs` there.

> [!WARNING]
> You will need to update the `docs/config.json` file (in `TanStack/config`) if you add a new documentation page!

You can see the whole process in the screen capture below:

https://github.com/fulopkovacs/form/assets/43729152/9d35a3c3-8153-4e74-9cb2-af275f7a269b

### Running examples

- Make sure you've installed the dependencies in the repo's root directory.

  ```bash
  pnpm install
  ```

- If you want to run the example against your local changes, run below in the repo's root directory. Otherwise, it will be run against the latest TanStack Config release.

  ```bash
  pnpm run watch
  ```

- Run below in the selected examples' directory.

  ```bash
  pnpm run dev
  ```

#### Note on standalone execution

If you want to run an example without installing dependencies for the whole repo, just follow instructions from the example's README.md file. It will be then run against the latest TanStack Config release.

## Online one-click setup

You can use Gitpod (An Online Open Source VS Code like IDE which is free for Open Source) for developing online. With a single click it will start a workspace and automatically:

- clone the `TanStack/config` repo.
- install all the dependencies in `/` and `/docs`.
- run below in the root(`/`) to Auto-build files.

  ```bash
  npm start
  ```

- run below in `/docs`.

  ```bash
  npm run dev
  ```

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/TanStack/config)

## Changesets

This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run run `pnpm changeset` and commit the file. If needed, changeset descriptions can be more descriptive, and will be included in the changelog. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset.

## Pull requests

Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface.

Use an appropriate commit type. Be especially careful with breaking changes.

## Releases

For each new commit added to `main`, a GitHub Workflow is triggered which runs the [Changesets Action](https://github.com/changesets/action). This generates a preview PR showing the impact of all changesets. When this PR is merged, the package will be published to NPM.


## Links discovered
- [GitHub Discussions](https://github.com/TanStack/config/discussions)
- [file an issue](https://github.com/TanStack/config/issues/new/choose)
- [pnpm](https://pnpm.io/)
- [nvm](https://github.com/nvm-sh/nvm)
- [tanstack.com](https://tanstack.com)
- [`TanStack/config`](https://github.com/TanStack/config)
- [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com)
- [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)
- [Changesets](https://github.com/changesets/changesets)
- [Changesets Action](https://github.com/changesets/action)

--- packages/eslint-config/CHANGELOG.md ---
# @tanstack/eslint-config

## 0.3.4

### Patch Changes

- chore(eslint): move to tsdown ([#325](https://github.com/TanStack/config/pull/325))

## 0.3.3

### Patch Changes

- chore: update deps ([#315](https://github.com/TanStack/config/pull/315))

- fix: add types ([#313](https://github.com/TanStack/config/pull/313))

## 0.3.2

### Patch Changes

- chore: update dependencies ([#285](https://github.com/TanStack/config/pull/285))

## 0.3.1

### Patch Changes

- fix: remove pnpm rules ([#276](https://github.com/TanStack/config/pull/276))

## 0.3.0

### Minor Changes

- chore: update @stylistic/eslint-plugin ([#267](https://github.com/TanStack/config/pull/267))

- feat: add eslint-plugin-pnpm ([#267](https://github.com/TanStack/config/pull/267))

## 0.2.0

### Minor Changes

- @stylistic/eslint-plugin-js to @stylistic/eslint-plugin as eslint-plugin-js is now deprecated ([#248](https://github.com/TanStack/config/pull/248))

## 0.1.0

### Minor Changes

- [#221](https://github.com/TanStack/config/pull/221) [`83d5dbc`](https://github.com/TanStack/config/commit/83d5dbc885c7533b0fc45b34873692be51c4eb25) Thanks [@lachlancollins](https://github.com/lachlancollins)! - Release @tanstack/eslint-config


## Links discovered
- [#325](https://github.com/TanStack/config/pull/325)
- [#315](https://github.com/TanStack/config/pull/315)
- [#313](https://github.com/TanStack/config/pull/313)
- [#285](https://github.com/TanStack/config/pull/285)
- [#276](https://github.com/TanStack/config/pull/276)
- [#267](https://github.com/TanStack/config/pull/267)
- [#248](https://github.com/TanStack/config/pull/248)
- [#221](https://github.com/TanStack/config/pull/221)
- [`83d5dbc`](https://github.com/TanStack/config/commit/83d5dbc885c7533b0fc45b34873692be51c4eb25)
- [@lachlancollins](https://github.com/lachlancollins)

--- packages/publish-config/CHANGELOG.md ---
# @tanstack/publish-config

## 0.2.2

### Patch Changes

- chore: update deps ([#315](https://github.com/TanStack/config/pull/315))

## 0.2.1

### Patch Changes

- chore: update dependencies ([#285](https://github.com/TanStack/config/pull/285))

## 0.2.0

### Minor Changes

- don't remove package scripts before publish ([#258](https://github.com/TanStack/config/pull/258))

## 0.1.1

### Patch Changes

- fix(publish): use 'previous' npm tag for old releases ([#249](https://github.com/TanStack/config/pull/249))

## 0.1.0

### Minor Changes

- [#222](https://github.com/TanStack/config/pull/222) [`43aae6e`](https://github.com/TanStack/config/commit/43aae6efe2642634e1ce1867b80b15a8cc829ac6) Thanks [@lachlancollins](https://github.com/lachlancollins)! - Release @tanstack/publish-config


## Links discovered
- [#315](https://github.com/TanStack/config/pull/315)
- [#285](https://github.com/TanStack/config/pull/285)
- [#258](https://github.com/TanStack/config/pull/258)
- [#249](https://github.com/TanStack/config/pull/249)
- [#222](https://github.com/TanStack/config/pull/222)
- [`43aae6e`](https://github.com/TanStack/config/commit/43aae6efe2642634e1ce1867b80b15a8cc829ac6)
- [@lachlancollins](https://github.com/lachlancollins)

--- packages/typedoc-config/CHANGELOG.md ---
# @tanstack/typedoc-config

## 0.3.3

### Patch Changes

- build: migrate to tsdown ([#327](https://github.com/TanStack/config/pull/327))

## 0.3.2

### Patch Changes

- fix: warning when not using the 'exclude' option ([#318](https://github.com/TanStack/config/pull/318))

## 0.3.1

### Patch Changes

- chore: update deps ([#315](https://github.com/TanStack/config/pull/315))

## 0.3.0

### Minor Changes

- feat: update to typedoc v0.28 ([#306](https://github.com/TanStack/config/pull/306))

- BREAKING: typedoc output is no longer lower-cased due to API changes ([#306](https://github.com/TanStack/config/pull/306))

## 0.2.1

### Patch Changes

- chore: update dependencies ([#285](https://github.com/TanStack/config/pull/285))

## 0.2.0

### Minor Changes

- Pin typedoc dependency versions ([#232](https://github.com/TanStack/config/pull/232))

## 0.1.0

### Minor Changes

- [#220](https://github.com/TanStack/config/pull/220) [`5ca7b1f`](https://github.com/TanStack/config/commit/5ca7b1fa45206cb83f95aee4cd784cdc8c1f377b) Thanks [@lachlancollins](https://github.com/lachlancollins)! - Release @tanstack/typedoc-config


## Links discovered
- [#327](https://github.com/TanStack/config/pull/327)
- [#318](https://github.com/TanStack/config/pull/318)
- [#315](https://github.com/TanStack/config/pull/315)
- [#306](https://github.com/TanStack/config/pull/306)
- [#285](https://github.com/TanStack/config/pull/285)
- [#232](https://github.com/TanStack/config/pull/232)
- [#220](https://github.com/TanStack/config/pull/220)
- [`5ca7b1f`](https://github.com/TanStack/config/commit/5ca7b1fa45206cb83f95aee4cd784cdc8c1f377b)
- [@lachlancollins](https://github.com/lachlancollins)

--- packages/vite-config/CHANGELOG.md ---
# @tanstack/vite-config

## 0.4.3

### Patch Changes

- chore: use publishConfig exports ([#325](https://github.com/TanStack/config/pull/325))

## 0.4.2

### Patch Changes

- chore: build with tsdown ([#321](https://github.com/TanStack/config/pull/321))

## 0.4.1

### Patch Changes

- chore: update deps ([#315](https://github.com/TanStack/config/pull/315))

## 0.4.0

### Minor Changes

- feat: support for bundling dependencies ([#302](https://github.com/TanStack/config/pull/302))

## 0.3.0

### Minor Changes

- feat: update to vite v7 ([#292](https://github.com/TanStack/config/pull/292))

## 0.2.1

### Patch Changes

- chore: update dependencies ([#285](https://github.com/TanStack/config/pull/285))

## 0.2.0

### Minor Changes

- Add `beforeWriteDeclarationFile` callback ([#215](https://github.com/TanStack/config/pull/215))

## 0.1.0

### Minor Changes

- [#222](https://github.com/TanStack/config/pull/222) [`43aae6e`](https://github.com/TanStack/config/commit/43aae6efe2642634e1ce1867b80b15a8cc829ac6) Thanks [@lachlancollins](https://github.com/lachlancollins)! - Release @tanstack/vite-config


## Links discovered
- [#325](https://github.com/TanStack/config/pull/325)
- [#321](https://github.com/TanStack/config/pull/321)
- [#315](https://github.com/TanStack/config/pull/315)
- [#302](https://github.com/TanStack/config/pull/302)
- [#292](https://github.com/TanStack/config/pull/292)
- [#285](https://github.com/TanStack/config/pull/285)
- [#215](https://github.com/TanStack/config/pull/215)
- [#222](https://github.com/TanStack/config/pull/222)
- [`43aae6e`](https://github.com/TanStack/config/commit/43aae6efe2642634e1ce1867b80b15a8cc829ac6)
- [@lachlancollins](https://github.com/lachlancollins)

--- .github/comment-on-release/comment-on-release.ts ---
#!/usr/bin/env node

import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { execSync } from 'node:child_process'

interface PublishedPackage {
  name: string
  version: string
}

interface PRInfo {
  number: number
  packages: Array<{ name: string; pkgPath: string; version: string }>
}

interface IssueInfo {
  number: number
  prs: Set<number>
  packages: Array<{ name: string; pkgPath: string; version: string }>
}

/**
 * Parse CHANGELOG.md to extract PR numbers from the latest version entry
 */
function extractPRsFromChangelog(
  changelogPath: string,
  version: string,
): Array<number> {
  try {
    const content = readFileSync(changelogPath, 'utf-8')
    const lines = content.split('\n')

    let inTargetVersion = false
    let foundVersion = false
    const prNumbers = new Set<number>()

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]

      // Check for version header (e.g., "## 0.21.0")
      if (line.startsWith('## ')) {
        const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/)
        if (versionMatch) {
          if (versionMatch[1] === version) {
            inTargetVersion = true
            foundVersion = true
          } else if (inTargetVersion) {
            // We've moved to the next version, stop processing
            break
          }
        }
      }

      // Extract PR numbers from links like [#302](https://github.com/TanStack/config/pull/302)
      if (inTargetVersion) {
        const prMatches = line.matchAll(
          /\[#(\d+)\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\)/g,
        )
        for (const match of prMatches) {
          prNumbers.add(parseInt(match[1], 10))
        }
      }
    }

    if (!foundVersion) {
      console.warn(
        `Warning: Could not find version ${version} in ${changelogPath}`,
      )
    }

    return Array.from(prNumbers)
  } catch (error) {
    console.error(`Error reading changelog at ${changelogPath}:`, error)
    return []
  }
}

/**
 * Group PRs by their numbers and collect all packages they contributed to
 */
function groupPRsByNumber(
  publishedPackages: Array<PublishedPackage>,
): Map<number, PRInfo> {
  const prMap = new Map<number, PRInfo>()

  for (const pkg of publishedPackages) {
    const pkgPath = `packages/${pkg.name.replace('@tanstack/', '')}`
    const changelogPath = resolve(process.cwd(), pkgPath, 'CHANGELOG.md')

    const prNumbers = extractPRsFromChangelog(changelogPath, pkg.version)

    for (const prNumber of prNumbers) {
      if (!prMap.has(prNumber)) {
        prMap.set(prNumber, { number: prNumber, packages: [] })
      }
      prMap.get(prNumber)!.packages.push({
        name: pkg.name,
        pkgPath: pkgPath,
        version: pkg.version,
      })
    }
  }

  return prMap
}

/**
 * Check if we've already commented on a PR/issue to avoid duplicates
 */
function hasExistingComment(number: number, type: 'pr' | 'issue'): boolean {
  try {
    const result = execSync(
      `gh api repos/\${GITHUB_REPOSITORY}/issues/${number}/comments --jq '[.[] | select(.body | contains("has been released!"))] | length'`,
      { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
    )
    const count = parseInt(result.trim(), 10)
    return count > 0
  } catch (error) {
    console.warn(
      `Warning: Could not check existing comments for ${type} #${number}`,
    )
    return false
  }
}

/**
 * Find issues that a PR closes/fixes using GitHub's GraphQL API
 */
function findLinkedIssues(prNumber: number, repository: string): Array<number> {
  const [owner, repo] = repository.split('/')
  const query = `
    query($owner: String!, $repo: String!, $pr: Int!) {
      repository(owner: $owner, name: $repo) {
        pullRequest(number: $pr) {
          closingIssuesReferences(first: 10) {
            nodes {
              number
            }
          }
        }
      }
    }
  `

  try {
    const result = execSync(
      `gh api graphql -f query='${query}' -F owner='${owner}' -F repo='${repo}' -F pr=${prNumber} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`,
      { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
    )

    const issueNumbers = result
      .trim()
      .split('\n')
      .filter((line) => line)
      .map((line) => parseInt(line, 10))

    if (issueNumbers.length > 0) {
      console.log(
        `  PR #${prNumber} links to issues: ${issueNumbers.join(', ')}`,
      )
    }

    return issueNumbers
  } catch (error) {
    return []
  }
}

/**
 * Post a comment on a GitHub PR using gh CLI
 */
async function commentOnPR(pr: PRInfo, repository: string): Promise<void> {
  const { number, packages } = pr

  // Check for duplicate comments
  if (hasExistingComment(number, 'pr')) {
    console.log(`↷ Already commented on PR #${number}, skipping`)
    return
  }

  // Build the comment body
  let comment = `🎉 This PR has been released!\n\n`

  for (const pkg of packages) {
    // Link to the package's changelog and version anchor
    const changelogUrl = `https://github.com/${repository}/blob/main/${pkg.pkgPath}/CHANGELOG.md#${pkg.version.replace(/\./g, '')}`
    comment += `- [${pkg.name}@${pkg.version}](${changelogUrl})\n`
  }

  comment += `\nThank you for your contribution!`

  try {
    // Use gh CLI to post the comment
    execSync(`gh pr comment ${number} --body '${comment.replace(/'/g, '"')}'`, {
      stdio: 'inherit',
    })
    console.log(`✓ Commented on PR #${number}`)
  } catch (error) {
    console.error(`✗ Failed to comment on PR #${number}:`, error)
  }
}

/**
 * Post a comment on a GitHub issue using gh CLI
 */
async function commentOnIssue(
  issue: IssueInfo,
  repository: string,
): Promise<void> {
  const { number, prs, packages } = issue

  // Check for duplicate comments
  if (hasExistingComment(number, 'issue')) {
    console.log(`↷ Already commented on issue #${number}, skipping`)
    return
  }

  const prLinks = Array.from(prs)
    .map((pr) => `#${pr}`)
    .join(', ')
  const prWord = prs.size === 1 ? 'PR' : 'PRs'

  // Build the comment body
  let comment = `🎉 The ${prWord} fixing this issue (${prLinks}) has been released!\n\n`

  for (const pkg of packages) {
    // Link to the package's changelog and version anchor
    const changelogUrl = `https://github.com/${repository}/blob/main/${pkg.pkgPath}/CHANGELOG.md#${pkg.version.replace(/\./g, '')}`
    comment += `- [${pkg.name}@${pkg.version}](${changelogUrl})\n`
  }

  comment += `\nThank you for reporting!`

  try {
    // Use gh CLI to post the comment
    execSync(
      `gh issue comment ${number} --body '${comment.replace(/'/g, '"')}'`,
      { stdio: 'inherit' },
    )
    console.log(`✓ Commented on issue #${number}`)
  } catch (error) {
    console.error(`✗ Failed to comment on issue #${number}:`, error)
  }
}

/**
 * Main function
 */
async function main() {
  // Read published packages from environment variable (set by GitHub Actions)
  const publishedPackagesJson = process.env.PUBLISHED_PACKAGES
  const repository = process.env.REPOSITORY

  if (!publishedPackagesJson) {
    console.log('No packages were published. Skipping PR comments.')
    return
  }

  if (!repository) {
    console.log('Repository is missing. Skipping PR comments.')
    return
  }

  let publishedPackages: Array<PublishedPackage>
  try {
    publishedPackages = JSON.parse(publishedPackagesJson)
  } catch (error) {
    console.error('Failed to parse PUBLISHED_PACKAGES:', error)
    process.exit(1)
  }

  if (publishedPackages.length === 0) {
    console.log('No packages were published. Skipping PR comments.')
    return
  }

  console.log(`Processing ${publishedPackages.length} published package(s)...`)

  // Group PRs by number
  const prMap = groupPRsByNumber(publishedPackages)

  if (prMap.size === 0) {
    console.log('No PRs found in CHANGELOGs. Nothing to comment on.')
    return
  }

  console.log(`Found ${prMap.size} PR(s) to comment on...`)

  // Collect issues linked to PRs
  const issueMap = new Map<number, IssueInfo>()

  // Comment on each PR and collect linked issues
  for (const pr of prMap.values()) {
    await commentOnPR(pr, repository)

    // Find issues that this PR closes/fixes
    const linkedIssues = findLinkedIssues(pr.number, repository)
    for (const issueNumber of linkedIssues) {
      if (!issueMap.has(issueNumber)) {
        issueMap.set(issueNumber, {
          number: issueNumber,
          prs: new Set(),
          packages: [],
        })
      }
      const issueInfo = issueMap.get(issueNumber)!
      issueInfo.prs.add(pr.number)

      // Merge packages, avoiding duplicates
      for (const pkg of pr.packages) {
        if (
          !issueInfo.packages.some(
            (p) => p.name === pkg.name && p.version === pkg.version,
          )
        ) {
          issueInfo.packages.push(pkg)
        }
      }
    }
  }

  if (issueMap.size > 0) {
    console.log(`\nFound ${issueMap.size} linked issue(s) to comment on...`)

    // Comment on each linked issue
    for (const issue of issueMap.values()) {
      await commentOnIssue(issue, repository)
    }
  }

  console.log('\n✓ Done!')
}

main().catch((error) => {
  console.error('Fatal error:', error)
  process.exit(1)
})


## Links discovered
- [#302](https://github.com/TanStack/config/pull/302)
- [${pkg.name}@${pkg.version}](https://github.com/TanStack/config/blob/main/.github/comment-on-release/${changelogUrl}.md)

--- .github/pull_request_template.md ---
## 🎯 Changes

<!-- What changes are made in this PR? Describe the change and its motivation. -->

## ✅ Checklist

- [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/config/blob/main/CONTRIBUTING.md).
- [ ] I have tested this code locally with `pnpm test:pr`.

## 🚀 Release Impact

- [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
- [ ] This change is docs/CI/dev-only (no release).


## Links discovered
- [Contributing guide](https://github.com/TanStack/config/blob/main/CONTRIBUTING.md)
- [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)

--- README.md ---
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />

<div align="center">
  <img src="./media/header_config.png" alt="TanStack Config" >
</div>

<br />

<div align="center">
<a href="https://www.npmjs.com/package/@tanstack/config" target="\_parent">
  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/config.svg" alt="npm downloads" />
</a>
<a href="https://github.com/TanStack/config/" target="\_parent">
  <img alt="" src="https://img.shields.io/github/stars/TanStack/config.svg?style=social&label=Star" alt="github stars" />
</a>
</div>

<div align="center">
<a href="#badge">
    <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
  </a>
	<a href="#badge">
		<img src="https://img.shields.io/github/v/release/tanstack/config" alt="Release"/>
	</a>
  <img src="https://img.shields.io/twitter/follow/tan_stack.svg?style=social" alt="Follow @TanStack"/>
</a>
</div>

<div align="center">
  
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
</div>

# TanStack Config

An opinionated toolkit for building, versioning, and publishing high‑quality JS/TS packages with minimal setup and consistent results.

- Vite‑powered builds with extendable workflows
- Automated publishing, versioning & changelogs
- Publint‑compliant with sensible defaults
- Minimal configuration for faster, hassle‑free setup

### <a href="https://tanstack.com/config">Read the docs →</b></a>

## Get Involved

- We welcome issues and pull requests!
- Participate in [GitHub discussions](https://github.com/TanStack/config/discussions)
- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ)
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions

## Partners

<table align="center">
  <tr>
    <td>
      <a href="https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS" >
        <picture>
          <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-CMcuvjEy.svg" height="40" />
          <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" />
          <img src="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" alt="CodeRabbit" />
        </picture>
      </a>
    </td>
    <td>
      <a href="https://www.cloudflare.com?utm_source=tanstack">
        <picture>
          <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-DQDB7UaL.svg" height="60" />
          <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" />
          <img src="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" alt="Cloudflare" />
        </picture>
      </a>
    </td>
  </tr>
</table>

<div align="center">
<img src="./media/partner_logo.svg" alt="Config & you?" height="65">
<p>
We're looking for TanStack Config Partners to join our mission! Partner with us to push the boundaries of TanStack Config and build amazing things together.
</p>
<a href="mailto:partners@tanstack.com?subject=TanStack Config Partnership"><b>LET'S CHAT</b></a>
</div>

## Explore the TanStack Ecosystem

- <a href="https://github.com/tanstack/db"><b>TanStack DB</b></a> – Reactive sync client store
- <a href="https://github.com/tanstack/devtools"><b>TanStack DevTools</b></a> – Unified devtools panel
- <a href="https://github.com/tanstack/form"><b>TanStack Form</b></a> – Type‑safe form state
- <a href="https://github.com/tanstack/pacer"><b>TanStack Pacer</b></a> – Debouncing, throttling, batching
- <a href="https://github.com/tanstack/query"><b>TanStack Query</b></a> – Async state & caching
- <a href="https://github.com/tanstack/ranger"><b>TanStack Ranger</b></a> – Range & slider primitives
- <a href="https://github.com/tanstack/router"><b>TanStack Router</b></a> – Type‑safe routing, caching & URL state
- <a href="https://github.com/tanstack/router"><b>TanStack Start</b></a> – Full‑stack SSR & streaming
- <a href="https://github.com/tanstack/store"><b>TanStack Store</b></a> – Reactive data store
- <a href="https://github.com/tanstack/table"><b>TanStack Table</b></a> – Headless datagrids
- <a href="https://github.com/tanstack/virtual"><b>TanStack Virtual</b></a> – Virtualized rendering

… and more at <a href="https://tanstack.com"><b>TanStack.com »</b></a>

<!-- Use the force, Luke! -->


## Links discovered
- [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
- [GitHub discussions](https://github.com/TanStack/config/discussions)
- [Discord](https://discord.com/invite/WrRKjPJ)
- [CONTRIBUTING.md](https://github.com/TanStack/config/blob/main/CONTRIBUTING.md)
- [<img alt="" src="https://img.shields.io/npm/dm/@tanstack/config.svg" alt="npm downloads" />](https://www.npmjs.com/package/@tanstack/config)
- [<img alt="" src="https://img.shields.io/github/stars/TanStack/config.svg?style=social&label=Star" alt="github stars" />](https://github.com/TanStack/config/)
- [Read the docs →</b>](https://tanstack.com/config)
- [<picture> <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-CMcuvjEy.svg" height="40" /> <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" /> <img src="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" alt="CodeRabbit" /> </picture>](https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS)
- [<picture> <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-DQDB7UaL.svg" height="60" /> <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" /> <img src="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" alt="Cloudflare" /> </picture>](https://www.cloudflare.com?utm_source=tanstack)
- [<b>TanStack DB</b>](https://github.com/tanstack/db)
- [<b>TanStack DevTools</b>](https://github.com/tanstack/devtools)
- [<b>TanStack Form</b>](https://github.com/tanstack/form)
- [<b>TanStack Pacer</b>](https://github.com/tanstack/pacer)
- [<b>TanStack Query</b>](https://github.com/tanstack/query)
- [<b>TanStack Ranger</b>](https://github.com/tanstack/ranger)
- [<b>TanStack Router</b>](https://github.com/tanstack/router)
- [<b>TanStack Start</b>](https://github.com/tanstack/router)
- [<b>TanStack Store</b>](https://github.com/tanstack/store)
- [<b>TanStack Table</b>](https://github.com/tanstack/table)
- [<b>TanStack Virtual</b>](https://github.com/tanstack/virtual)
- [<b>TanStack.com »</b>](https://tanstack.com)

--- prettier.config.js ---
// @ts-check

/** @type {import('prettier').Config} */
const config = {
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
}

export default config


--- scripts/verify-links.ts ---
import { existsSync, readFileSync, statSync } from 'node:fs'
import { extname, resolve } from 'node:path'
import { glob } from 'tinyglobby'
// @ts-ignore Could not find a declaration file for module 'markdown-link-extractor'.
import markdownLinkExtractor from 'markdown-link-extractor'

const errors: Array<{
  file: string
  link: string
  resolvedPath: string
  reason: string
}> = []

function isRelativeLink(link: string) {
  return (
    !link.startsWith(`/`) &&
    !link.startsWith('http://') &&
    !link.startsWith('https://') &&
    !link.startsWith('//') &&
    !link.startsWith('#') &&
    !link.startsWith('mailto:')
  )
}

/** Remove any trailing .md */
function stripExtension(p: string): string {
  return p.replace(`${extname(p)}`, '')
}

function relativeLinkExists(link: string, file: string): boolean {
  // Remove hash if present
  const linkWithoutHash = link.split('#')[0]
  // If the link is empty after removing hash, it's not a file
  if (!linkWithoutHash) return false

  // Strip the file/link extensions
  const filePath = stripExtension(file)
  const linkPath = stripExtension(linkWithoutHash)

  // Resolve the path relative to the markdown file's directory
  // Nav up a level to simulate how links are resolved on the web
  let absPath = resolve(filePath, '..', linkPath)

  // Ensure the resolved path is within /docs
  const docsRoot = resolve('docs')
  if (!absPath.startsWith(docsRoot)) {
    errors.push({
      link,
      file,
      resolvedPath: absPath,
      reason: 'Path outside /docs',
    })
    return false
  }

  // Check if this is an example path
  const isExample = absPath.includes('/examples/')

  let exists = false

  if (isExample) {
    // Transform /docs/framework/{framework}/examples/ to /examples/{framework}/
    absPath = absPath.replace(
      /\/docs\/framework\/([^/]+)\/examples\//,
      '/examples/$1/',
    )
    // For examples, we want to check if the directory exists
    exists = existsSync(absPath) && statSync(absPath).isDirectory()
  } else {
    // For non-examples, we want to check if the .md file exists
    if (!absPath.endsWith('.md')) {
      absPath = `${absPath}.md`
    }
    exists = existsSync(absPath)
  }

  if (!exists) {
    errors.push({
      link,
      file,
      resolvedPath: absPath,
      reason: 'Not found',
    })
  }
  return exists
}

async function verifyMarkdownLinks() {
  // Find all markdown files in docs directory
  const markdownFiles = await glob('docs/**/*.md', {
    ignore: ['**/node_modules/**'],
  })

  console.log(`Found ${markdownFiles.length} markdown files\n`)

  // Process each file
  for (const file of markdownFiles) {
    const content = readFileSync(file, 'utf-8')
    const links: Array<string> = markdownLinkExtractor(content)

    const relativeLinks = links.filter((link: string) => {
      return isRelativeLink(link)
    })

    if (relativeLinks.length > 0) {
      relativeLinks.forEach((link) => {
        relativeLinkExists(link, file)
      })
    }
  }

  if (errors.length > 0) {
    console.log(`\n❌ Found ${errors.length} broken links:`)
    errors.forEach((err) => {
      console.log(
        `${err.file}\n  link:      ${err.link}\n  resolved:  ${err.resolvedPath}\n  why:       ${err.reason}\n`,
      )
    })
    process.exit(1)
  } else {
    console.log('\n✅ No broken links found!')
  }
}

verifyMarkdownLinks().catch(console.error)


--- integrations/react/vite.config.ts ---
import { defineConfig, mergeConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { tanstackViteConfig } from '@tanstack/vite-config'

const config = defineConfig({
  plugins: [react()],
  test: {
    name: 'react-integration',
    watch: false,
  },
})

export default mergeConfig(
  config,
  tanstackViteConfig({
    entry: './src/index.ts',
    srcDir: './src',
  }),
)


--- integrations/vanilla/vite.config.ts ---
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'

const config = defineConfig({
  test: {
    name: 'vanilla-integration',
    watch: false,
  },
})

export default mergeConfig(
  config,
  tanstackViteConfig({
    entry: './src/index.ts',
    srcDir: './src',
  }),
)


--- integrations/vue/vite.config.ts ---
import { defineConfig, mergeConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { tanstackViteConfig } from '@tanstack/vite-config'

const config = defineConfig({
  plugins: [vue()],
  test: {
    name: 'vue-integration',
    watch: false,
  },
})

export default mergeConfig(
  config,
  tanstackViteConfig({
    entry: './src/index.ts',
    srcDir: './src',
  }),
)


--- integrations/react/tests/build.test.ts ---
import { readFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'

const __dirname = dirname(fileURLToPath(import.meta.url))
const rootDir = resolve(__dirname, '..')

const esmExtensions = ['.js', '.js.map', '.d.ts']
const cjsExtensions = ['.cjs', '.cjs.map', '.d.cts']

const files = ['index', 'use-client', 'nested/nested']

describe('check React build output', () => {
  it('should output the same file structure', () => {
    const distFiles = readdirSync(`${rootDir}/dist`, { recursive: true })
    const snapFiles = readdirSync(`${rootDir}/snap`, { recursive: true })

    expect(distFiles).toEqual(snapFiles)
  })

  it('should build the same ESM output', () => {
    files.forEach((file) => {
      esmExtensions.forEach((ext) => {
        expect(
          readFileSync(`${rootDir}/dist/esm/${file}${ext}`).toString(),
        ).toMatchFileSnapshot(`${rootDir}/snap/esm/${file}${ext}`)
      })
    })
  })

  it('should build the same CJS output', () => {
    files.forEach((file) => {
      cjsExtensions.forEach((ext) => {
        expect(
          readFileSync(`${rootDir}/dist/cjs/${file}${ext}`).toString(),
        ).toMatchFileSnapshot(`${rootDir}/snap/cjs/${file}${ext}`)
      })
    })
  })
})


--- integrations/vanilla/tests/build.test.ts ---
import { readFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'

const __dirname = dirname(fileURLToPath(import.meta.url))
const rootDir = resolve(__dirname, '..')

const esmExtensions = ['.js', '.js.map', '.d.ts']
const cjsExtensions = ['.cjs', '.cjs.map', '.d.cts']

const files = ['index', 'utils']

describe('check Vanilla build output', () => {
  it('should output the same file structure', () => {
    const distFiles = readdirSync(`${rootDir}/dist`, { recursive: true })
    const snapFiles = readdirSync(`${rootDir}/snap`, { recursive: true })

    expect(distFiles).toEqual(snapFiles)
  })

  it('should build the same ESM output', () => {
    files.forEach((file) => {
      esmExtensions.forEach((ext) => {
        expect(
          readFileSync(`${rootDir}/dist/esm/${file}${ext}`).toString(),
        ).toMatchFileSnapshot(`${rootDir}/snap/esm/${file}${ext}`)
      })
    })
  })

  it('should build the same CJS output', () => {
    files.forEach((file) => {
      cjsExtensions.forEach((ext) => {
        expect(
          readFileSync(`${rootDir}/dist/cjs/${file}${ext}`).toString(),
        ).toMatchFileSnapshot(`${rootDir}/snap/cjs/${file}${ext}`)
      })
    })
  })
})


--- integrations/vue/tests/build.test.ts ---
import { readFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'

const __dirname = dirname(fileURLToPath(import.meta.url))
const rootDir = resolve(__dirname, '..')

const esmExtensions = ['.js', '.js.map', '.d.ts']
const cjsExtensions = ['.cjs', '.cjs.map', '.d.cts']

const files = ['index', 'App.vue', 'components/HelloWorld.vue']

describe('check Vite build output', () => {
  it('should output the same file structure', () => {
    const distFiles = readdirSync(`${rootDir}/dist`, { recursive: true })
    const snapFiles = readdirSync(`${rootDir}/snap`, { recursive: true })

    expect(distFiles).toEqual(snapFiles)
  })

  it('should build the same ESM output', () => {
    files.forEach((file) => {
      esmExtensions.forEach((ext) => {
        expect(
          readFileSync(`${rootDir}/dist/esm/${file}${ext}`).toString(),
        ).toMatchFileSnapshot(`${rootDir}/snap/esm/${file}${ext}`)
      })
    })
  })

  it('should build the same CJS output', () => {
    files.forEach((file) => {
      cjsExtensions.forEach((ext) => {
        expect(
          readFileSync(`${rootDir}/dist/cjs/${file}${ext}`).toString(),
        ).toMatchFileSnapshot(`${rootDir}/snap/cjs/${file}${ext}`)
      })
    })
  })
})


--- integrations/vanilla/src/dynamic.ts ---
export const foo = 'HELLO'


--- integrations/react/src/index.ts ---
export * from '@tanstack/query-core'
export * from './use-client'
export { test } from './nested/nested'


--- integrations/vanilla/src/index.ts ---
export * from './utils'


--- integrations/vue/src/index.ts ---
export { default as App } from './App.vue'


--- packages/eslint-config/eslint.config.ts ---
// @ts-check

import { tanstackConfig } from '@tanstack/eslint-config'

export default [...tanstackConfig]


--- packages/publish-config/eslint.config.ts ---
// @ts-check

import { tanstackConfig } from '@tanstack/eslint-config'

export default [...tanstackConfig]


--- packages/typedoc-config/eslint.config.ts ---
// @ts-check

import { tanstackConfig } from '@tanstack/eslint-config'

export default [...tanstackConfig]


--- packages/vite-config/eslint.config.ts ---
// @ts-check

import { tanstackConfig } from '@tanstack/eslint-config'

export default [...tanstackConfig]


--- packages/eslint-config/tsdown.config.ts ---
import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['./src/index.ts'],
  format: ['esm'],
  unbundle: true,
  dts: true,
  sourcemap: true,
  clean: true,
  minify: false,
  fixedExtension: false,
  exports: {
    devExports: true,
  },
  publint: {
    strict: true,
  },
  attw: {
    profile: 'esm-only',
    level: 'error',
  },
})


--- packages/typedoc-config/tsdown.config.ts ---
import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['./src/index.ts', './src/typedoc-custom-settings.ts'],
  format: ['esm'],
  unbundle: true,
  dts: true,
  sourcemap: true,
  clean: true,
  minify: false,
  fixedExtension: false,
  exports: true,
  publint: {
    strict: true,
  },
  attw: {
    profile: 'esm-only',
    level: 'error',
  },
})


--- packages/vite-config/tsdown.config.ts ---
import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['./src/index.ts'],
  format: ['esm'],
  unbundle: true,
  dts: true,
  sourcemap: true,
  clean: true,
  minify: false,
  fixedExtension: false,
  exports: {
    devExports: true,
  },
  publint: {
    strict: true,
  },
  attw: {
    profile: 'esm-only',
    level: 'error',
  },
})


--- packages/eslint-config/src/import.ts ---
import type { Linter } from 'eslint'

/**
 * @see https://github.com/un-ts/eslint-plugin-import-x
 */
export const importRules: Linter.RulesRecord = {
  /** Bans the use of inline type-only markers for named imports */
  'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
  /** Reports any imports that come after non-import statements */
  'import/first': 'error',
  /** Stylistic preference */
  'import/newline-after-import': 'error',
  /** No require() or module.exports */
  'import/no-commonjs': 'error',
  /** Reports if a resolved path is imported more than once */
  'import/no-duplicates': 'error',
  /** Stylistic preference */
  'import/order': [
    'error',
    {
      groups: [
        'builtin',
        'external',
        'internal',
        'parent',
        'sibling',
        'index',
        'object',
        'type',
      ],
    },
  ],
}


--- packages/eslint-config/src/index.ts ---
import tseslint from 'typescript-eslint'
import vueParser from 'vue-eslint-parser'
import stylisticPlugin from '@stylistic/eslint-plugin'
import importPlugin from 'eslint-plugin-import-x'
import nodePlugin from 'eslint-plugin-n'
import globals from 'globals'
import { javascriptRules } from './javascript.js'
import { importRules } from './import.js'
import { typescriptRules } from './typescript.js'
import { nodeRules } from './node.js'
import { stylisticRules } from './stylistic.js'
import type { Linter } from 'eslint'

const GLOB_EXCLUDE = [
  '**/.nx/**',
  '**/.svelte-kit/**',
  '**/build/**',
  '**/coverage/**',
  '**/dist/**',
  '**/snap/**',
  '**/vite.config.*.timestamp-*.*',
]

const jsRules = {
  ...javascriptRules,
  ...typescriptRules,
  ...importRules,
  ...nodeRules,
  ...stylisticRules,
}

const jsPlugins = {
  '@stylistic': stylisticPlugin,
  '@typescript-eslint': tseslint.plugin,
  import: importPlugin,
  node: nodePlugin,
}

export const tanstackConfig: Array<Linter.Config> = [
  {
    name: 'tanstack/ignores',
    ignores: GLOB_EXCLUDE,
  },
  {
    name: 'tanstack/javascript',
    files: ['**/*.{js,ts,tsx}'],
    languageOptions: {
      sourceType: 'module',
      ecmaVersion: 2020,
      parser: tseslint.parser,
      parserOptions: {
        project: true,
        parser: tseslint.parser,
      },
      globals: {
        ...globals.browser,
      },
    },
    // @ts-expect-error
    plugins: jsPlugins,
    rules: jsRules,
  },
  {
    name: 'tanstack/vue',
    files: ['**/*.vue'],
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        sourceType: 'module',
        ecmaVersion: 2020,
        parser: tseslint.parser,
        project: true,
        extraFileExtensions: ['.vue'],
      },
      globals: {
        ...globals.browser,
      },
    },
    // @ts-expect-error
    plugins: jsPlugins,
    rules: jsRules,
  },
]


--- packages/publish-config/src/index.js ---
// @ts-check
// Originally ported to TS from https://github.com/remix-run/react-router/tree/main/scripts/{version,publish}.js

import path from 'node:path'
import { execSync } from 'node:child_process'
import { existsSync, readdirSync } from 'node:fs'
import { platform } from 'node:os'
import * as semver from 'semver'
import { parse as parseCommit } from '@commitlint/parse'
import { simpleGit } from 'simple-git'
import {
  capitalize,
  getSorterFn,
  readPackageJson,
  releaseCommitMsg,
  updatePackageJson,
} from './utils.js'

function currentGitBranch() {
  let stdout

  try {
    let cmd = ''

    if (platform() === 'win32') {
      cmd = `git branch | findstr \\*`
    } else {
      cmd = `git branch | grep \\*`
    }

    stdout = execSync(cmd).toString()
  } catch (e) {
    console.error(e)
    return false
  }

  const branchName = stdout.slice(2, stdout.length).trim()

  return branchName
}

/**
 * Execute a script being published
 * @param {import('./index.js').Options} options
 * @returns {Promise<void>}
 */
export const publish = async (options) => {
  const { branchConfigs, packages, rootDir, branch, tag, ghToken } = options

  const branchName = /** @type {string} */ (branch ?? currentGitBranch())
  const isMainBranch = branchName === 'main'

  /** @type {import('./index.js').BranchConfig | undefined} */
  const branchConfig = branchConfigs[branchName]

  if (!branchConfig) {
    throw new Error(`No publish config found for branch: ${branchName}`)
  }

  // Get tags
  /** @type {string[]} */
  const allTags = execSync('git tag').toString().split('\n')

  const filteredTags = allTags
    // Ensure tag is valid
    .filter((t) => semver.valid(t))
    // sort by latest
    .sort(semver.compare)
    // Filter tags to our branch/pre-release combo
    .filter((t) => {
      // If this is an older release, filter to only include that version
      if (branchConfig.previousVersion) {
        return t.startsWith(branchName)
      }
      if (semver.prerelease(t) === null) {
        return isMainBranch
      } else {
        return !isMainBranch
      }
    })

  // Get the latest tag
  let latestTag = filteredTags.at(-1)

  let rangeFrom = latestTag

  // If RELEASE_ALL is set via a commit subject or body, all packages will be
  // released regardless if they have changed files matching the package srcDir.
  let RELEASE_ALL = false

  // Validate manual tag
  if (tag) {
    if (!semver.valid(tag)) {
      throw new Error(`tag '${tag}' is not a semantically valid version`)
    }
    if (!tag.startsWith('v')) {
      throw new Error(
        `tag must start with "v" (e.g. v0.0.0). You supplied ${tag}`,
      )
    }
    if (allTags.includes(tag)) {
      throw new Error(`tag ${tag} has already been released`)
    }
  }

  if (!latestTag || tag) {
    if (tag) {
      console.info(
        `Tag is set to ${tag}. This will force release all packages. Publishing...`,
      )
      RELEASE_ALL = true

      // Is it the first release? Is it a major version?
      if (!latestTag || (semver.patch(tag) === 0 && semver.minor(tag) === 0)) {
        rangeFrom = 'origin/main'
        latestTag = tag
      }
    } else {
      throw new Error(
        'Could not find latest tag! To make a release tag of v0.0.1, run with TAG=v0.0.1',
      )
    }
  }

  console.info(`Git Range: ${rangeFrom}..HEAD`)

  const rawCommitsLog = (
    await simpleGit().log({ from: rangeFrom, to: 'HEAD' })
  ).all.filter((c) => {
    const exclude = [
      c.message.startsWith('Merge branch '), // No merge commits
      c.message.startsWith(releaseCommitMsg('')), // No example update commits
    ].some(Boolean)

    return !exclude
  })

  /**
   * Get the commits since the latest tag
   * @type {import('./index.js').Commit[]}
   */
  const commitsSinceLatestTag = await Promise.all(
    rawCommitsLog.map(async (c) => {
      const parsed = await parseCommit(c.message)
      return {
        hash: c.hash.substring(0, 7),
        body: c.body,
        subject: parsed.subject ?? '',
        author_name: c.author_name,
        author_email: c.author_email,
        type: parsed.type?.toLowerCase() ?? 'other',
        scope: parsed.scope,
      }
    }),
  )

  console.info(
    `Parsing ${commitsSinceLatestTag.length} commits since ${rangeFrom}...`,
  )

  /**
   * Parses the commit messsages, log them, and determine the type of release needed
   * -1 means no release is necessary
   * 0 means patch release is necessary
   * 1 means minor release is necessary
   * 2 means major release is necessary
   * @type {number}
   */
  let recommendedReleaseLevel = commitsSinceLatestTag.reduce(
    (releaseLevel, commit) => {
      if (commit.type) {
        if (['fix', 'refactor', 'perf'].includes(commit.type)) {
          releaseLevel = Math.max(releaseLevel, 0)
        }
        if (['feat'].includes(commit.type)) {
          releaseLevel = Math.max(releaseLevel, 1)
        }
        if (commit.body.includes('BREAKING CHANGE')) {
          releaseLevel = Math.max(releaseLevel, 2)
        }
      }
      return releaseLevel
    },
    -1,
  )

  // If there is a breaking change and no manual tag is set, do not release
  if (recommendedReleaseLevel === 2 && !tag) {
    throw new Error(
      'Major versions releases must be tagged and released manually.',
    )
  }

  // If no release is semantically necessary and no manual tag is set, do not release
  if (recommendedReleaseLevel === -1 && !tag) {
    console.info(
      `There have been no changes since ${latestTag} that require a new version. You're good!`,
    )
    return
  }

  // If no release is samantically necessary but a manual tag is set, do a patch release
  if (recommendedReleaseLevel === -1 && tag) {
    recommendedReleaseLevel = 0
  }

  const releaseType = /** @type {const} */ ({
    0: 'patch',
    1: 'minor',
    2: 'major',
  })[recommendedReleaseLevel]

  if (!releaseType) {
    throw new Error(`Invalid release level: ${recommendedReleaseLevel}`)
  }

  const version = tag
    ? semver.parse(tag)?.version
    : branchConfig.prerelease
      ? semver.inc(latestTag, 'prerelease', branchName)
      : semver.inc(latestTag, releaseType)

  if (!version) {
    throw new Error(
      [
        'Invalid version increment from semver.inc()',
        `- latestTag: ${latestTag}`,
        `- recommendedReleaseLevel: ${recommendedReleaseLevel}`,
        `- prerelease: ${branchConfig.prerelease}`,
      ].join('\n'),
    )
  }

  console.log(`Targeting version ${version}...`)

  /**
   * Uses git diff to determine which files have changed since the latest tag
   * @type {string[]}
   */
  const changedFiles = tag
    ? []
    : execSync(`git diff ${latestTag} --name-only`)
        .toString()
        .split('\n')
        .filter(Boolean)

  /** Uses packages and changedFiles to determine which packages have changed */
  const changedPackages = RELEASE_ALL
    ? packages
    : packages.filter((pkg) => {
        const changed = changedFiles.some(
          (file) =>
            file.startsWith(path.join(pkg.packageDir, 'src')) ||
            file.startsWith(path.join(pkg.packageDir, 'package.json')),
        )
        return changed
      })

  // If a package has a dependency that has been updated, we need to update the
  // package that depends on it as well.
  // run this multiple times so that dependencies of dependencies are also included
  for (let runs = 0; runs < 3; runs++) {
    for (const pkg of packages) {
      const packageJson = await readPackageJson(
        path.resolve(rootDir, pkg.packageDir, 'package.json'),
      )
      const allDependencies = Object.keys(
        Object.assign(
          {},
          packageJson.dependencies ?? {},
          packageJson.peerDependencies ?? {},
        ),
      )

      if (
        allDependencies.find((dep) =>
          changedPackages.find((d) => d.name === dep),
        ) &&
        !changedPackages.find((d) => d.name === pkg.name)
      ) {
        console.info(`  Adding dependency ${pkg.name} to changed packages`)
        changedPackages.push(pkg)
      }
    }
  }

  const changelogCommitsMd = await Promise.all(
    Object.entries(
      commitsSinceLatestTag.reduce((prev, curr) => {
        return {
          ...prev,
          [curr.type]: [...(prev[curr.type] ?? []), curr],
        }
      }, /** @type {Record<string, import('./index.js').Commit[]>} */ ({})),
    )
      .sort(
        getSorterFn(([type]) =>
          [
            'other',
            'examples',
            'docs',
            'ci',
            'test',
            'chore',
            'refactor',
            'perf',
            'fix',
            'feat',
          ].indexOf(type),
        ),
      )
      .reverse()
      .map(async ([type, commits]) => {
        return Promise.all(
          commits.map(async (commit) => {
            let username = ''

            if (ghToken) {
              const query = commit.author_email

              const res = await fetch(
                `https://api.github.com/search/users?q=${query}`,
                {
                  headers: {
                    Authorization: `token ${ghToken}`,
                  },
                },
              )
              const data = /** @type {unknown} */ (await res.json())
              if (data && typeof data === 'object' && 'items' in data) {
                if (Array.isArray(data.items) && data.items[0]) {
                  const item = /** @type {object} */ (data.items[0])
                  if ('login' in item && typeof item.login === 'string') {
                    username = item.login
                  }
                }
              }
            }

            const scope = commit.scope ? `${commit.scope}: ` : ''
            const subject = commit.subject

            return `- ${scope}${subject} (${commit.hash}) ${
              username
                ? `by @${username}`
                : `by ${commit.author_name || commit.author_email}`
            }`
          }),
        ).then((c) => /** @type {const} */ ([type, c]))
      }),
  ).then((groups) => {
    return groups
      .map(([type, commits]) => {
        return [`### ${capitalize(type)}`, commits.join('\n')].join('\n\n')
      })
      .join('\n\n')
  })

  const date = new Intl.DateTimeFormat(undefined, {
    dateStyle: 'short',
    timeStyle: 'short',
  }).format(Date.now())

  const changelogMd = [
    `Version ${version} - ${date}${tag ? ' (Manual Release)' : ''}`,
    '## Changes',
    changelogCommitsMd || '- None',
    '## Packages',
    changedPackages.map((d) => `- ${d.name}@${version}`).join('\n'),
  ].join('\n\n')

  console.info('Generating changelog...')
  console.info()
  console.info(changelogMd)
  console.info()

  if (changedPackages.length === 0) {
    console.info('No packages have been affected.')
    return
  }

  console.info(`Updating all changed packages to version ${version}...`)
  // Update each package to the new version
  for (const pkg of changedPackages) {
    console.info(`  Updating ${pkg.name} version to ${version}...`)

    await updatePackageJson(
      path.resolve(rootDir, pkg.packageDir, 'package.json'),
      (config) => {
        config.version = version
      },
    )
  }

  if (existsSync(path.resolve(rootDir, 'examples'))) {
    console.info('Updating examples to use new package versions...')
    const examplePkgJsonArray = /** @type {string[]} */ (
      readdirSync(path.resolve(rootDir, 'examples'), {
        recursive: true,
      }).filter(
        (file) =>
          typeof file === 'string' &&
          file.includes('package.json') &&
          !file.includes('node_modules'),
      )
    )
    if (examplePkgJsonArray.length !== 0) {
      for (const examplePkgJson of examplePkgJsonArray) {
        await updatePackageJson(
          path.resolve(rootDir, 'examples', examplePkgJson),
          (config) => {
            for (const pkg of changedPackages) {
              if (config.dependencies?.[pkg.name]) {
                config.dependencies[pkg.name] = `^${version}`
              }
              if (config.devDependencies?.[pkg.name]) {
                config.devDependencies[pkg.name] = `^${version}`
              }
            }
          },
        )
      }
      if (existsSync(path.resolve(rootDir, 'pnpm-lock.yaml'))) {
        console.info('  Updating pnpm-lock.yaml...')
        try {
          execSync('pnpm install --no-frozen-lockfile')
        } catch (/** @type {any} */ err) {
          throw new Error(err.stdout.toString())
        }
      }
    }
  }

  if (!process.env.CI) {
    console.warn(
      `This is a dry run for version ${version}. Push to CI to publish for real or set CI=true to override!`,
    )
    return
  }

  console.info()
  console.info('Committing changes...')
  execSync(`git add -A && git commit -m "${releaseCommitMsg(version)}"`)
  console.info('  Committed Changes.')

  /** 'latest' for current version, 'previous' for old versions, and custom for prereleases */
  const npmTag = isMainBranch
    ? 'latest'
    : branchConfig.previousVersion
      ? 'previous'
      : branchName

  console.info()
  console.info(`Publishing all packages to npm with tag "${npmTag}"`)

  // Publish each package
  for (const pkg of changedPackages) {
    const packageDir = path.join(rootDir, pkg.packageDir)

    const cmd = `cd ${packageDir} && pnpm publish --tag ${npmTag} --access=public --no-git-checks`
    console.info(`  Publishing ${pkg.name}@${version} to npm...`)
    execSync(cmd, {
      stdio: [process.stdin, process.stdout, process.stderr],
    })
  }

  console.info()
  console.info('Pushing changes...')
  execSync('git push')
  console.info('  Changes pushed.')

  console.info()
  console.info(`Creating new git tag v${version}`)
  execSync(`git tag -a -m "v${version}" v${version}`)

  console.info()
  console.info('Pushing tags...')
  execSync('git push --tags')
  console.info('  Tags pushed.')

  if (ghToken) {
    console.info()
    console.info('Creating github release...')

    // Stringify the markdown to escape any quotes
    execSync(
      `gh release create v${version} ${
        branchConfig.prerelease ? '--prerelease' : ''
      } --notes '${changelogMd.replace(/'/g, '"')}'`,
      { env: { ...process.env, GH_TOKEN: ghToken } },
    )
    console.info('  Github release created.')
  }

  console.info()
  console.info('All done!')
}
