[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-1720":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":23,"hasPages":23,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":28,"readmeContent":29,"aiSummary":30,"trendingCount":16,"starSnapshotCount":16,"syncStatus":17,"lastSyncTime":31,"discoverSource":32},1720,"tsnapi","antfu\u002Ftsnapi","antfu","Library public API snapshot testing for runtime exports and type declarations.","",null,"TypeScript",185,3,171,1,0,2,4,13,6,1.81,"MIT License",false,"main",[26,27],"snapshot","snapshot-testing","2026-06-12 02:00:31","\u003Cimg src=\".\u002Ftsnapi.svg\" alt=\"tsnapi\" height=\"150\" \u002F>\n\n# tsnapi\n\n\nLibrary public API snapshot testing for runtime exports and type declarations.\n\nCaptures your public API surface -- both runtime exports and type declarations -- into human-readable snapshot files that you commit alongside your code. When the API changes unexpectedly, you'll know.\n\nThink of it like Vitest's snapshot testing, but for your package's public contract.\n\nFor example, you can check the generated snapshots for the package itself: [`__snapshots__\u002Ftsnapi`](\u002F__snapshots__\u002Ftsnapi).\n\n## Why\n\nWhen maintaining a library, it's easy to accidentally:\n\n- Remove or rename an export\n- Change a function signature\n- Break type declarations\n- Introduce unintended public API surface\n\n`tsnapi` makes these changes visible in your git diff. Every build produces a pair of snapshot files per entry point:\n\n- **`.snapshot.js`** -- what your package exports at runtime\n- **`.snapshot.d.ts`** -- what your package exports as types\n\nThese files are committed to your repo. When they change, you review the diff -- just like any other code change.\n\n## Install\n\n```bash\npnpm add -D tsnapi\n```\n\n## Usage\n\n### As a Rolldown \u002F tsdown plugin\n\nThe most recommended way of using `tsnapi` is to use it with [`tsdown`](https:\u002F\u002Ftsdown.dev) -- an elegant library bundler built on top of Rolldown:\n\n```ts\n\u002F\u002F tsdown.config.ts\nimport { defineConfig } from 'tsdown'\nimport ApiSnapshot from 'tsnapi\u002Frolldown'\n\nexport default defineConfig({\n  entry: ['src\u002Findex.ts'],\n  dts: true,\n  plugins: [\n    ApiSnapshot()\n  ],\n})\n```\n\nOn first build, snapshot files are written. On subsequent builds, the plugin compares against existing snapshots and **fails the build with a diff** if the API changed.\n\nTo update snapshots when you intentionally change the API, set the `update` option to `true` or use the `--update-snapshot` \u002F `-u` CLI flag:\n\n```bash\ntsdown --update-snapshot\n# or\nUPDATE_SNAPSHOT=1 tsdown\n```\n\nor add the `update` option to the plugin:\n\n\u003C!-- eslint-skip -->\n```ts\nplugins: [\n  ApiSnapshot({ update: true })\n],\n```\n\n### As a CLI\n\nSnapshot any package's dist without a bundler:\n\n```bash\n# Snapshot the current package (reads package.json exports → parses dist files), which you need to run the build first to generate the dist\ntsnapi\n\n# Update snapshots when you intentionally change the API\ntsnapi -u\n```\n\n### With Vitest\n\n`tsnapi\u002Fvitest` provides higher-level Vitest integration that uses [`toMatchFileSnapshot`](https:\u002F\u002Fvitest.dev\u002Fguide\u002Fsnapshot#file-snapshots) to store snapshots as individual files.\n\n> **Note:** `tsnapi` reads built dist files, so make sure to build your packages before running the tests. We recommend updating your test script to build first:\n>\n> ```json\n> {\n>   \"scripts\": {\n>     \"test\": \"pnpm run build && vitest\"\n>   }\n> }\n> ```\n>\n> If you are using [`tsdown`](https:\u002F\u002Ftsdown.dev), you can use [`tsdown-stale-guard`](https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Ftsdown-stale-guard) to set up the build guard automatically.\n\n#### Single package\n\n```ts\n\u002F\u002F api.test.ts\nimport { fileURLToPath } from 'node:url'\nimport { snapshotApiPerEntry } from 'tsnapi\u002Fvitest'\nimport { describe } from 'vitest'\n\nconst dir = fileURLToPath(new URL('..\u002Fpackages\u002Fmy-lib', import.meta.url))\n\ndescribe('my-lib API', async () => {\n  await snapshotApiPerEntry(dir)\n})\n```\n\nThis creates `it()` blocks for each entry point, asserting both runtime and DTS snapshots. Snapshot files are written to `__snapshots__\u002Ftsnapi\u002F\u003Cpackage-name>\u002F` relative to the test file. Run `vitest -u` to update snapshots when you intentionally change the API.\n\n#### Monorepo\n\nFor monorepos, `describePackagesApiSnapshots` creates a `describe()` block per package. When `packages` is omitted, it auto-discovers workspace packages from `pnpm-workspace.yaml` or the `workspaces` field in `package.json`:\n\n```ts\n\u002F\u002F api.test.ts\nimport { describePackagesApiSnapshots } from 'tsnapi\u002Fvitest'\n\n\u002F\u002F Auto-discovers all workspace packages\nawait describePackagesApiSnapshots()\n```\n\nOr provide explicit package paths:\n\n```ts\nimport { fileURLToPath } from 'node:url'\nimport { describePackagesApiSnapshots } from 'tsnapi\u002Fvitest'\n\nawait describePackagesApiSnapshots({\n  packages: [\n    fileURLToPath(new URL('..\u002Fpackages\u002Fcore', import.meta.url)),\n    fileURLToPath(new URL('..\u002Fpackages\u002Futils', import.meta.url)),\n  ],\n})\n```\n\n`describePackagesApiSnapshots` accepts `filter`, `beforeEach`, and `afterEach` callbacks. Each receives a `PackageContext` object:\n\n```ts\ninterface PackageContext {\n  cwd: string \u002F\u002F the input cwd\n  workspaceRoot: string \u002F\u002F the resolved workspace root\n  packageRoot: string \u002F\u002F absolute path to the package directory\n  packageName: string \u002F\u002F package name from package.json\n  outputDir: string \u002F\u002F snapshot output directory (relative to test file)\n}\n```\n\n#### `filter`\n\nCalled for each discovered package. The context is mutable — modify any property to customize behavior. Return `false` to skip the package entirely:\n\n```ts\nimport { describePackagesApiSnapshots } from 'tsnapi\u002Fvitest'\n\nawait describePackagesApiSnapshots({\n  filter(ctx) {\n    \u002F\u002F Skip private packages\n    if (ctx.packageName.startsWith('@internal\u002F'))\n      return false\n    \u002F\u002F Strip org scope from describe block name\n    ctx.packageName = ctx.packageName.replace(\u002F^@.*\\\u002F\u002F, '')\n    \u002F\u002F Customize snapshot output directory per package\n    ctx.outputDir = `__snapshots__\u002F${ctx.packageName}`\n  },\n})\n```\n\n#### `beforeEach` \u002F `afterEach`\n\nLifecycle hooks registered inside each package's `describe` block via Vitest's `beforeEach`\u002F`afterEach`. They receive the (possibly mutated) context:\n\n```ts\nimport { describePackagesApiSnapshots } from 'tsnapi\u002Fvitest'\n\nawait describePackagesApiSnapshots({\n  beforeEach({ packageRoot, packageName }) {\n    console.log(`Testing ${packageName} at ${packageRoot}`)\n  },\n  afterEach({ packageName }) {\n    console.log(`Done testing ${packageName}`)\n  },\n})\n```\n\nFor example, if you use [`tsdown-stale-guard`](https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Ftsdown-stale-guard), you can use the `beforeEach` hook to guard against stale builds — ensuring each package's dist is in sync with its source before running snapshot tests:\n\n```ts\nimport { guardStaleBuild } from 'tsdown-stale-guard'\nimport { describePackagesApiSnapshots } from 'tsnapi\u002Fvitest'\n\nawait describePackagesApiSnapshots({\n  async beforeEach({ packageRoot }) {\n    \u002F\u002F guard will throw if the build is stale to fail the test\n    await guardStaleBuild({ root: packageRoot })\n  },\n})\n```\n\nRun `vitest -u` to update snapshots when you intentionally change the API.\n\n#### Low-level\n\nYou can also use `generateApiSnapshot` directly with Vitest's built-in snapshot system:\n\n```ts\n\u002F\u002F api.test.ts\nimport { generateApiSnapshot } from 'tsnapi'\nimport { expect, it } from 'vitest'\n\nconst api = await generateApiSnapshot(process.cwd())\n\nit('runtime API', () => {\n  expect(api['.'].runtime).toMatchInlineSnapshot()\n})\n\nit('type declarations', () => {\n  expect(api['.'].dts).toMatchInlineSnapshot()\n})\n```\n\nFor packages with multiple entry points, each entry is keyed by its export path:\n\n```ts\nconst api = await generateApiSnapshot(process.cwd())\nexpect(api['.\u002Futils'].runtime).toMatchSnapshot()\n```\n\n### As a library\n\n```ts\nimport { snapshotPackage } from 'tsnapi'\n\n\u002F\u002F Snapshot current package\nconst result = await snapshotPackage(process.cwd())\n\nif (result.hasChanges) {\n  console.error(result.diff)\n}\n```\n\n## Options\n\n```ts\ninterface ApiSnapshotOptions {\n  \u002F** Snapshot output directory. @default '__snapshots__\u002Ftsnapi' *\u002F\n  outputDir?: string\n  \u002F** Runtime snapshot extension. @default '.snapshot.js' *\u002F\n  extensionRuntime?: string\n  \u002F** DTS snapshot extension. @default '.snapshot.d.ts' *\u002F\n  extensionDts?: string\n  \u002F** Omit argument names from function signatures. @default true *\u002F\n  omitArgumentNames?: boolean\n  \u002F** Widen literal types to base types, hiding implementation details. @default true *\u002F\n  typeWidening?: boolean\n  \u002F** Update mode. Auto-detected from --update-snapshot \u002F -u \u002F UPDATE_SNAPSHOT=1 *\u002F\n  update?: boolean\n}\n```\n\n### `typeWidening`\n\nWhen `typeWidening` is `true` (default), literal values are widened to hide implementation details:\n\n- **Runtime**: `export const VERSION = '1.0.0'` → `export var VERSION \u002F* const *\u002F`\n- **DTS**: `declare const VERSION = \"1.0.0\"` → `export declare const VERSION: string;`\n\nWhen `typeWidening` is `false`, literal values are preserved in the snapshot:\n\n- **Runtime**: `export const VERSION = '1.0.0'` → `export var VERSION = '1.0.0' \u002F* const *\u002F`\n- **DTS**: `declare const VERSION = \"1.0.0\"` → `export declare const VERSION = \"1.0.0\";`\n\nThis applies to string, number, boolean, null, bigint, and array literals. Non-literal values (function calls, complex objects) are always stripped regardless of this setting.\n\n## Credits\n\nThis project is heavily inspired by:\n\n- [rolldown-plugin-dts-snapshot](https:\u002F\u002Fgithub.com\u002Fsxzz\u002Frolldown-plugin-dts-snapshot) by [@sxzz](https:\u002F\u002Fgithub.com\u002Fsxzz) -- DTS snapshot approach using AST parsing\n- [vitest-package-exports](https:\u002F\u002Fgithub.com\u002Fantfu\u002Fvitest-package-exports) by [@antfu](https:\u002F\u002Fgithub.com\u002Fantfu) -- Concept of snapshotting package exports for regression detection\n\n## License\n\n[MIT](.\u002FLICENSE)\n","tsnapi 是一个用于库公共 API 快照测试的工具，支持运行时导出和类型声明的快照。它能够捕获你的公共 API 表面，并将其转换为可读性强的快照文件，与代码一起提交到版本控制系统中。当 API 发生意外变化时，这些快照可以帮助开发者快速发现并审查变更。tsnapi 通过生成 `.snapshot.js` 和 `.snapshot.d.ts` 文件来分别记录运行时导出和类型声明的变化，确保任何对公共接口的修改都能在 git diff 中清晰可见。适合于需要维护稳定公共 API 的 TypeScript 库开发场景，特别是对于那些希望避免无意间破坏 API 合约的项目来说非常有用。","2026-06-11 02:45:38","CREATED_QUERY"]