chore: import zh skill ts-library

This commit is contained in:
wehub-skill-sync
2026-07-13 21:37:05 +08:00
commit 8dd69a3f6c
21 changed files with 3356 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# WeHub 来源说明
- Skill 名称:`ts-library`
- 中文类目:TypeScript 库打包与发布
- 上游仓库:`onmax__nuxt-skills`
- 上游路径:`skills/ts-library/SKILL.md`
- 上游链接:https://github.com/onmax/nuxt-skills/blob/HEAD/skills/ts-library/SKILL.md
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
- 原作者、版权和许可证信息以上游仓库为准
+105
View File
@@ -0,0 +1,105 @@
---
name: ts-library
description: 用于编写 TypeScript 库或 npm 包时的技能——涵盖项目初始化、package.json 导出配置、构建工具(tsdown/unbuild)、API 设计模式、类型推断技巧、测试以及发布到 npm。适用于打包配置、双 CJS/ESM 输出配置或发布工作流设置。
license: MIT
---
# TypeScript 库开发
从 unocss、shiki、unplugin、vite、vitest、vueuse、zod、trpc、drizzle-orm 等项目中提炼出的高质量 TypeScript 库编写模式。
## 何时使用
- 开始一个新的 TypeScript 库(单体仓库或 monorepo
- 配置 package.json 的双 CJS/ESM 导出
- 为库开发配置 tsconfig
- 选择构建工具(tsdown、unbuild
- 设计类型安全的 APIbuilder、factory、plugin 模式)
- 编写高级 TypeScript 类型
- 为库测试配置 vitest
- 配置发布工作流与 CI
**Nuxt 模块开发:** 请使用 `nuxt-modules` 技能
## 快速参考
| 当前工作... | 加载文件 |
| --------------------- | ------------------------------------------------------------------ |
| 新建项目初始化 | [references/project-setup.md](references/project-setup.md) |
| 包导出配置 | [references/package-exports.md](references/package-exports.md) |
| tsconfig 选项 | [references/typescript-config.md](references/typescript-config.md) |
| 构建配置 | [references/build-tooling.md](references/build-tooling.md) |
| ESLint 配置 | [references/eslint-config.md](references/eslint-config.md) |
| API 设计模式 | [references/api-design.md](references/api-design.md) |
| 类型推断技巧 | [references/type-patterns.md](references/type-patterns.md) |
| 测试配置 | [references/testing.md](references/testing.md) |
| 发布工作流 | [references/release.md](references/release.md) |
| CI/CD 配置 | [references/ci-workflows.md](references/ci-workflows.md) |
## 加载文件
**请根据当前任务按需加载以下参考文件:**
- [ ] [references/project-setup.md](references/project-setup.md) —— 如果是在新建 TypeScript 库项目
- [ ] [references/package-exports.md](references/package-exports.md) —— 如果是在配置 package.json 导出或双 CJS/ESM
- [ ] [references/typescript-config.md](references/typescript-config.md) —— 如果是在设置或修改 tsconfig.json
- [ ] [references/build-tooling.md](references/build-tooling.md) —— 如果是在配置 tsdown、unbuild 或构建脚本
- [ ] [references/eslint-config.md](references/eslint-config.md) —— 如果是在为库开发设置 ESLint
- [ ] [references/api-design.md](references/api-design.md) —— 如果是在设计公共 API、builder 模式或插件系统
- [ ] [references/type-patterns.md](references/type-patterns.md) —— 如果是在处理高级 TypeScript 类型或类型推断
- [ ] [references/testing.md](references/testing.md) —— 如果是在配置 vitest 或编写库代码的测试
- [ ] [references/release.md](references/release.md) —— 如果是在配置发布工作流或版本管理
- [ ] [references/ci-workflows.md](references/ci-workflows.md) —— 如果是在配置 GitHub Actions 或 CI/CD 管道
**不要一次性加载所有文件。** 只加载与当前任务相关的文件。
## 新建库的工作流程
1. 创建项目结构 → 加载 [references/project-setup.md](references/project-setup.md)
2. 配置 `package.json` 导出 → 加载 [references/package-exports.md](references/package-exports.md)
3. 使用 tsdown 配置构建 → 加载 [references/build-tooling.md](references/build-tooling.md)
4. 验证构建:`pnpm build && pnpm pack --dry-run` —— 检查输出是否包含 `.mjs``.cjs``.d.ts`
5. 添加测试 → 加载 [references/testing.md](references/testing.md)
6. 配置发布 → 加载 [references/release.md](references/release.md)
## 快速开始
```json
// package.json(最小配置)
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": ["dist"]
}
```
```ts
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
```
## 核心原则
- ESM 优先:使用 `"type": "module"` 配合 `.mjs` 输出
- 双格式:始终同时支持 CJS 和 ESM 消费者
- `moduleResolution: "Bundler"` 用于现代 TypeScript
- 大部分构建使用 tsdown,复杂场景使用 unbuild
- 智能默认值:自动检测环境,不强制要求配置
- 支持摇树优化:惰性 getter,正确设置 `sideEffects: false`
_令牌效率:主技能约 300 token,每个参考文件约 8001200 token_
+187
View File
@@ -0,0 +1,187 @@
# 构建工具
## 工具选择
| 工具 | 适用场景 |
| ------------------- | -------------------------------------------- |
| **tsdown** | 大多数库 —— 快速、简单、现代化 |
| **unbuild** | 复杂构建、Nuxt 模块、自动外部化 |
| **rollup/rolldown** | 需要精细控制的大型项目 |
## tsdown(推荐)
```bash
pnpm add -D tsdown
```
### 基本配置
```typescript
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### 多入口
```typescript
export default defineConfig({
entry: ['src/index.ts', 'src/cli.ts', 'src/utils.ts'],
format: ['esm', 'cjs'],
dts: true,
external: ['vue', 'vite'],
})
```
### 插件模式(unplugin-\*
```typescript
export default defineConfig({
entry: ['src/*.ts'], // 通配所有入口
format: ['esm', 'cjs'],
dts: true,
exports: true, // 自动生成 package.json exports
attw: { profile: 'esm-only' }, // 类型检查配置
})
```
### 高级选项
```typescript
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
resolve: ['@antfu/utils'], // 在声明文件中内联特定依赖
},
external: ['vue'],
define: {
__DEV__: 'false',
},
hooks: {
'build:done': async () => {
// 构建后任务
},
},
})
```
## unbuild
```bash
pnpm add -D unbuild
```
### 基本配置
```typescript
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
})
```
### 带外部化依赖
```typescript
export default defineBuildConfig({
entries: ['src/index', 'src/cli'],
declaration: true,
externals: ['vue', 'vite'],
rollup: {
emitCJS: true,
inlineDependencies: true,
dts: { respectExternal: true },
},
})
```
## 输出格式
### 仅 ESM(现代)
```typescript
export default defineConfig({
format: ['esm'],
})
```
### 双格式 CJS/ESM(推荐)
```typescript
export default defineConfig({
format: ['esm', 'cjs'],
})
```
### 带 IIFE 用于 CDN
```typescript
export default defineConfig([
{ format: ['esm', 'cjs'], dts: true },
{ format: 'iife', globalName: 'MyLib', minify: true },
])
```
## 定义标志
常用编译时标志:
```typescript
export default defineConfig({
define: {
__DEV__: `(process.env.NODE_ENV !== 'production')`,
__TEST__: 'false',
__BROWSER__: 'true',
__VERSION__: JSON.stringify(pkg.version),
},
})
```
## 构建脚本
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"prepublishOnly": "pnpm build"
}
}
```
## 故障排除
### CJS 默认导出问题
某些打包工具需要显式默认导出:
```typescript
export default defineConfig({
hooks: {
'build:done': async () => {
// 如有需要,修补 CJS 文件
},
},
})
```
### 输出中缺少类型声明
确保 `dts: true`,并检查 tsconfig 中的 `isolatedDeclarations`
### 外部化不生效
检查包是否在 `peerDependencies` 中,并已在 `external` 中列出。
+210
View File
@@ -0,0 +1,210 @@
# API 设计模式
## Options 模式
面向用户的选项及其内部解析后的版本:
```typescript
export interface Options {
verbose?: boolean
include?: string[]
exclude?: string[]
}
export interface ResolvedOptions extends Required<Options> {
root: string
}
function resolveOptions(options: Options = {}): ResolvedOptions {
return {
verbose: options.verbose ?? false,
include: options.include ?? ['**/*'],
exclude: options.exclude ?? ['node_modules'],
root: process.cwd(),
}
}
```
## 工厂函数
创建配置好的实例:
```typescript
export function createContext(options: Options = {}) {
const resolved = resolveOptions(options)
const filter = createFilter(resolved.include, resolved.exclude)
return {
options: resolved,
filter,
transform(code: string, id: string) { /* ... */ },
async scanDirs() { /* ... */ },
}
}
// 用法
const ctx = createContext({ verbose: true })
await ctx.scanDirs()
```
## Builder 模式
支持类型累积的可链式调用 API
```typescript
export function createBuilder<TContext = unknown>() {
return {
context<T>(): Builder<T, unknown, unknown> {
return this as any
},
input<T>(schema: T): Builder<TContext, T, unknown> {
return this as any
},
output<T>(schema: T): Builder<TContext, unknown, T> {
return this as any
},
build(): Procedure<TContext> { /* ... */ },
}
}
// 用法 - 类型在链中传递
const procedure = createBuilder()
.context<{ user: User }>()
.input(z.object({ id: z.string() }))
.build()
```
## 插件模式(unplugin
从单一实现生成通用插件:
```typescript
import { createUnplugin } from 'unplugin'
export default createUnplugin<Options>((options) => {
const ctx = createContext(options)
return {
name: 'my-plugin',
enforce: 'pre',
transformInclude(id) {
return ctx.filter(id)
},
transform(code, id) {
return ctx.transform(code, id)
},
// 特定于打包工具的钩子
vite: {
configResolved(config) { /* Vite 特有 */ },
},
webpack(compiler) {
compiler.hooks.watchRun.tap('my-plugin', () => { /* ... */ })
},
}
})
```
按打包工具分别导出入口:
```typescript
// src/vite.ts
import unplugin from '.'
export default unplugin.vite
// src/webpack.ts
import unplugin from '.'
export default unplugin.webpack
```
## 惰性 GetterTree-shaking
将特定于打包工具的代码延迟到被访问时才加载:
```typescript
export function createPlugin<T>(factory: PluginFactory<T>) {
return {
get vite() { return getVitePlugin(factory) },
get webpack() { return getWebpackPlugin(factory) },
get rollup() { return getRollupPlugin(factory) },
}
}
```
只有被访问的 getter 会执行,其余部分被 tree-shaking 移除。
## 智能默认值
检测环境而非要求配置:
```typescript
import { isPackageExists } from 'local-pkg'
function resolveOptions(options: Options) {
return {
vue: options.vue ?? isPackageExists('vue'),
react: options.react ?? isPackageExists('react'),
typescript: options.typescript ?? isPackageExists('typescript'),
}
}
```
## Resolver 模式
通过函数或对象实现灵活解析:
```typescript
export type Resolver = ResolverFunction | ResolverObject
export type ResolverFunction = (name: string) => ResolveResult | undefined
export interface ResolverObject {
type: 'component' | 'directive'
resolve: ResolverFunction
}
export function ElementPlusResolver(): Resolver[] {
return [
{ type: 'component', resolve: (name) => resolveComponent(name) },
{ type: 'directive', resolve: (name) => resolveDirective(name) },
]
}
```
## Fluent API(验证)
通过克隆实现不可变性的方法链:
```typescript
class Schema<T> {
private _def: SchemaDef
min(value: number): Schema<T> {
return new Schema({ ...this._def, min: value })
}
max(value: number): Schema<T> {
return new Schema({ ...this._def, max: value })
}
optional(): Schema<T | undefined> {
return new Schema({ ...this._def, optional: true })
}
}
// 用法
const schema = z.string().min(5).max(10).optional()
```
## Barrel 导出
简洁的公开 API
```typescript
// src/index.ts
export * from './config'
export * from './types'
export { createContext } from './context'
export { default } from './plugin'
```
+191
View File
@@ -0,0 +1,191 @@
# 类型模式
## 工具类型
各库中常用的辅助类型:
```typescript
// Promise 或同步
export type Awaitable<T> = T | Promise<T>
// 单个或数组
export type Arrayable<T> = T | T[]
// 可空
export type Nullable<T> = T | null | undefined
// 深度可选
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// 简化交叉类型以改善 IDE 显示
export type Simplify<T> = { [K in keyof T]: T[K] } & {}
// 阻止特定位置的类型推断
export type NoInfer<T> = [T][T extends any ? 0 : never]
```
## 条件提取
从结构中提取类型:
```typescript
// 从 schema 中提取输入类型
export type Input<T> = T extends { _input: infer U } ? U : unknown
// 提取输出类型
export type Output<T> = T extends { _output: infer U } ? U : unknown
// 从嵌套属性中提取
export type InferContext<T> = T extends { context: infer C } ? C : never
```
## 品牌类型
为原始类型添加名义类型:
```typescript
declare const brand: unique symbol
export type Brand<T, B> = T & { readonly [brand]: B }
export type UserId = Brand<string, 'UserId'>
export type PostId = Brand<string, 'PostId'>
// 不会混淆
function getUser(id: UserId) { /* ... */ }
getUser('abc' as UserId) // 正确
getUser('abc' as PostId) // 错误!
```
## 类型累积(构建器模式)
每个方法更新泛型参数:
```typescript
interface ProcedureBuilder<TContext, TInput, TOutput> {
input<T>(schema: T): ProcedureBuilder<TContext, T, TOutput>
output<T>(schema: T): ProcedureBuilder<TContext, TInput, T>
query(fn: (opts: { ctx: TContext; input: TInput }) => TOutput): Procedure
}
// 类型沿链式调用传递
const proc = builder
.input(z.object({ id: z.string() })) // TInput = { id: string }
.output(z.object({ name: z.string() })) // TOutput = { name: string }
.query(({ input }) => ({ name: input.id }))
```
## 模块扩充
允许用户扩展库类型:
```typescript
// 库代码
export interface Register {}
export type DefaultError = Register extends { defaultError: infer E }
? E
: Error
// 用户代码
declare module 'my-lib' {
interface Register {
defaultError: MyCustomError
}
}
```
## 数据标记
使用 symbol 附加类型元数据:
```typescript
declare const dataTagSymbol: unique symbol
declare const errorTagSymbol: unique symbol
export type DataTag<TType, TData, TError> = TType & {
[dataTagSymbol]: TData
[errorTagSymbol]: TError
}
// 提取标记类型
export type InferData<T> = T extends { [dataTagSymbol]: infer D } ? D : unknown
```
## 映射类型修饰
列构建器模式(drizzle):
```typescript
type NotNull<T extends ColumnBuilder> = T & { _: { notNull: true } }
type HasDefault<T extends ColumnBuilder> = T & { _: { hasDefault: true } }
class ColumnBuilder<T extends ColumnConfig> {
notNull(): NotNull<this> {
// ...
return this as NotNull<this>
}
default(value: T['data']): HasDefault<this> {
// ...
return this as HasDefault<this>
}
}
```
## 编译时错误
返回可读的错误信息:
```typescript
type TypeError<Message extends string> = { __error: Message }
type ValidateInput<T> = T extends string
? T
: TypeError<'Input must be a string'>
// 显示:类型 "TypeError<"Input must be a string">" 不可赋值给...
```
## 函数重载
为不同输入提供多个签名:
```typescript
export function useEventListener<E extends keyof WindowEventMap>(
event: E,
listener: (ev: WindowEventMap[E]) => any
): void
export function useEventListener<E extends keyof DocumentEventMap>(
target: Document,
event: E,
listener: (ev: DocumentEventMap[E]) => any
): void
export function useEventListener(...args: any[]) {
// 实现
}
```
## 分布式条件类型
应用于每个联合成员:
```typescript
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// Result = string[] | number[]
```
使用元组禁用分发:
```typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result = ToArrayNonDist<string | number>
// Result = (string | number)[]
```
+210
View File
@@ -0,0 +1,210 @@
# API 设计模式
## Options 模式
对外暴露可配置选项,内部使用已解析的确定版本:
```typescript
export interface Options {
verbose?: boolean
include?: string[]
exclude?: string[]
}
export interface ResolvedOptions extends Required<Options> {
root: string
}
function resolveOptions(options: Options = {}): ResolvedOptions {
return {
verbose: options.verbose ?? false,
include: options.include ?? ['**/*'],
exclude: options.exclude ?? ['node_modules'],
root: process.cwd(),
}
}
```
## 工厂函数
创建已配置的实例:
```typescript
export function createContext(options: Options = {}) {
const resolved = resolveOptions(options)
const filter = createFilter(resolved.include, resolved.exclude)
return {
options: resolved,
filter,
transform(code: string, id: string) { /* ... */ },
async scanDirs() { /* ... */ },
}
}
// 使用示例
const ctx = createContext({ verbose: true })
await ctx.scanDirs()
```
## Builder 模式
支持类型累积的可链式调用 API
```typescript
export function createBuilder<TContext = unknown>() {
return {
context<T>(): Builder<T, unknown, unknown> {
return this as any
},
input<T>(schema: T): Builder<TContext, T, unknown> {
return this as any
},
output<T>(schema: T): Builder<TContext, unknown, T> {
return this as any
},
build(): Procedure<TContext> { /* ... */ },
}
}
// 使用示例 —— 类型随链式调用流动
const procedure = createBuilder()
.context<{ user: User }>()
.input(z.object({ id: z.string() }))
.build()
```
## 插件模式(unplugin
通过单一实现生成通用插件:
```typescript
import { createUnplugin } from 'unplugin'
export default createUnplugin<Options>((options) => {
const ctx = createContext(options)
return {
name: 'my-plugin',
enforce: 'pre',
transformInclude(id) {
return ctx.filter(id)
},
transform(code, id) {
return ctx.transform(code, id)
},
// 各打包工具特有的钩子
vite: {
configResolved(config) { /* Vite 特有逻辑 */ },
},
webpack(compiler) {
compiler.hooks.watchRun.tap('my-plugin', () => { /* ... */ })
},
}
})
```
按打包工具分别导出入口:
```typescript
// src/vite.ts
import unplugin from '.'
export default unplugin.vite
// src/webpack.ts
import unplugin from '.'
export default unplugin.webpack
```
## 惰性 GetterTree-shaking
将打包工具特有代码延迟到被访问时才加载:
```typescript
export function createPlugin<T>(factory: PluginFactory<T>) {
return {
get vite() { return getVitePlugin(factory) },
get webpack() { return getWebpackPlugin(factory) },
get rollup() { return getRollupPlugin(factory) },
}
}
```
只有被访问的 getter 会执行,其余代码被 tree-shaking 移除。
## 智能默认值
自动检测环境,无需手动配置:
```typescript
import { isPackageExists } from 'local-pkg'
function resolveOptions(options: Options) {
return {
vue: options.vue ?? isPackageExists('vue'),
react: options.react ?? isPackageExists('react'),
typescript: options.typescript ?? isPackageExists('typescript'),
}
}
```
## Resolver 模式
支持函数或对象的灵活解析方式:
```typescript
export type Resolver = ResolverFunction | ResolverObject
export type ResolverFunction = (name: string) => ResolveResult | undefined
export interface ResolverObject {
type: 'component' | 'directive'
resolve: ResolverFunction
}
export function ElementPlusResolver(): Resolver[] {
return [
{ type: 'component', resolve: (name) => resolveComponent(name) },
{ type: 'directive', resolve: (name) => resolveDirective(name) },
]
}
```
## Fluent API(验证)
通过克隆实现不可变性的方法链:
```typescript
class Schema<T> {
private _def: SchemaDef
min(value: number): Schema<T> {
return new Schema({ ...this._def, min: value })
}
max(value: number): Schema<T> {
return new Schema({ ...this._def, max: value })
}
optional(): Schema<T | undefined> {
return new Schema({ ...this._def, optional: true })
}
}
// 使用示例
const schema = z.string().min(5).max(10).optional()
```
## Barrel 导出
简洁的公开 API
```typescript
// src/index.ts
export * from './config'
export * from './types'
export { createContext } from './context'
export { default } from './plugin'
```
+187
View File
@@ -0,0 +1,187 @@
# 构建工具链
## 工具选择
| 工具 | 使用场景 |
| ------------------- | -------------------------------------------- |
| **tsdown** | 大多数库——快速、简单、现代 |
| **unbuild** | 复杂构建、Nuxt 模块、自动外部化 |
| **rollup/rolldown** | 需要精细控制的大型项目 |
## tsdown(推荐)
```bash
pnpm add -D tsdown
```
### 基础配置
```typescript
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### 多入口
```typescript
export default defineConfig({
entry: ['src/index.ts', 'src/cli.ts', 'src/utils.ts'],
format: ['esm', 'cjs'],
dts: true,
external: ['vue', 'vite'],
})
```
### 插件模式(unplugin-\*
```typescript
export default defineConfig({
entry: ['src/*.ts'], // 使用 Glob 匹配所有入口
format: ['esm', 'cjs'],
dts: true,
exports: true, // 自动生成 package.json 中的 exports
attw: { profile: 'esm-only' }, // 类型检查配置
})
```
### 高级选项
```typescript
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
resolve: ['@antfu/utils'], // 在声明中内联特定依赖
},
external: ['vue'],
define: {
__DEV__: 'false',
},
hooks: {
'build:done': async () => {
// 构建后任务
},
},
})
```
## unbuild
```bash
pnpm add -D unbuild
```
### 基础配置
```typescript
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
})
```
### 带外部化配置
```typescript
export default defineBuildConfig({
entries: ['src/index', 'src/cli'],
declaration: true,
externals: ['vue', 'vite'],
rollup: {
emitCJS: true,
inlineDependencies: true,
dts: { respectExternal: true },
},
})
```
## 输出格式
### 仅 ESM(现代)
```typescript
export default defineConfig({
format: ['esm'],
})
```
### 双格式 CJS/ESM(推荐)
```typescript
export default defineConfig({
format: ['esm', 'cjs'],
})
```
### 带 IIFE 用于 CDN
```typescript
export default defineConfig([
{ format: ['esm', 'cjs'], dts: true },
{ format: 'iife', globalName: 'MyLib', minify: true },
])
```
## 编译时标志
常用的编译时标志:
```typescript
export default defineConfig({
define: {
__DEV__: `(process.env.NODE_ENV !== 'production')`,
__TEST__: 'false',
__BROWSER__: 'true',
__VERSION__: JSON.stringify(pkg.version),
},
})
```
## 构建脚本
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"prepublishOnly": "pnpm build"
}
}
```
## 故障排查
### CJS 默认导出问题
某些打包工具需要显式默认导出:
```typescript
export default defineConfig({
hooks: {
'build:done': async () => {
// 需要时修补 CJS 文件
},
},
})
```
### 输出中缺少类型声明
确保 `dts: true` 并检查 tsconfig 中的 `isolatedDeclarations`
### 外部化不生效
检查包是否在 `peerDependencies` 中并且已列入 `external`
+265
View File
@@ -0,0 +1,265 @@
# CI 工作流
## 基础 CI
```yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm test
```
## 矩阵测试
```yaml
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest]
node: [20, 22, 24]
include:
- os: macos-latest
node: 24
- os: windows-latest
node: 24
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- run: pnpm install
- run: pnpm test
```
## 跳过纯文档变更
```yaml
jobs:
changed:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.check.outputs.only_changed == 'true' }}
steps:
- uses: tj-actions/changed-files@v47
id: check
with:
files: |
docs/**
**.md
test:
needs: changed
if: needs.changed.outputs.should_skip != 'true'
# ... 其余任务配置
```
## 自动修复提交
```yaml
- run: pnpm lint:fix
- uses: stefanzweifel/git-auto-commit-action@v5
if: github.event_name == 'push'
with:
commit_message: 'chore: lint fix'
```
## 标签发布(基于 Token
```yaml
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install
- run: pnpm build
- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
## 标签发布(OIDC — 推荐做法)
无需 NPM_TOKEN。使用 GitHub OIDC 实现免 Token 认证并附带来源证明(provenance)。
```yaml
name: Release
permissions:
id-token: write
contents: write
actions: read
on:
push:
tags: ['v*']
jobs:
wait-for-ci:
runs-on: ubuntu-latest
steps:
- uses: lewagon/wait-on-check-action@v1.3.4
with:
ref: ${{ github.sha }}
check-name: ci
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
release:
needs: wait-for-ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24 # 必需:npm 11.5.1+
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install
- run: pnpm build
- run: pnpm dlx changelogithub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: pnpm publish --access public --no-git-checks --provenance
```
### OIDC 配置步骤
1. 打开 `https://www.npmjs.com/package/<PACKAGE_NAME>/access`
2. 滚动至「Publishing access」区域
3. 在「Trusted Publishers」下点击「Add GitHub Actions」
4. 填写:Owner(所有者)、Repository(仓库)、Workflow file(工作流文件,`release.yml`)、Environment(环境,留空)
5. 点击「Add」
### OIDC 要求
1. **Node.js 24+**(需要 npm 11.5.1+——Node 22 自带的 npm 10.x 会失败)
2. **权限(Permissions**`id-token: write`
3. **发布标志**`--provenance`
4. **package.json**:必须包含 `repository` 字段
5. **npm 两步验证(2FA**:设置为「需要 2FA 或粒度访问令牌」(允许 OIDC)
### 故障排查
| 错误信息 | 原因 | 修复方法 |
| --------------------------------------- | ------------------- | ------------------------------------------ |
| "Access token expired" E404 | npm 版本过旧 | 使用 Node.js 24 |
| ENEEDAUTH | 缺少 registry-url | 在 setup-node 中添加 `registry-url` |
| "repository.url is empty" E422 | 缺少字段 | 在 package.json 中添加 `repository` |
| "not configured as trusted publisher" | 配置不匹配 | 检查 owner、repo、workflow 是否完全一致 |
## 单体仓库矩阵
```yaml
jobs:
test:
strategy:
matrix:
package: [core, utils, cli]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm --filter ${{ matrix.package }} test
```
## 并发控制
取消过时的运行:
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
```
## 针对 PR 的 pkg-pr-new
```yaml
# .github/workflows/pkg-pr-new.yml
name: Publish PR
on: pull_request
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm build
- run: pnpm dlx pkg-pr-new publish --compact --pnpm
```
## CI 中的包验证
```yaml
- run: pnpm build
- run: pnpm dlx publint
- run: pnpm dlx @arethetypeswrong/cli --pack .
```
+120
View File
@@ -0,0 +1,120 @@
# @antfu/eslint-config
扁平 ESLint 配置,同时处理代码检查与格式化——取代 Prettier。
## 安装
```bash
pnpm add -D eslint @antfu/eslint-config
```
```js
// eslint.config.mjs
import antfu from '@antfu/eslint-config'
export default antfu()
```
```json
{ "scripts": { "lint": "eslint ." } }
```
## 配置选项
```js
import antfu from '@antfu/eslint-config'
export default antfu({
type: 'lib', // 'lib' 用于库,'app' 用于应用
ignores: ['**/fixtures', '**/dist'],
stylistic: { indent: 2, quotes: 'single' },
typescript: true, // 自动检测
vue: true, // 自动检测
})
```
## 框架支持
| 框架 | 选项 | 必需依赖包 |
| -------- | ------------------ | --------------------------------------------------------- |
| Vue | `vue: true` | (自动检测) |
| React | `react: true` | `@eslint-react/eslint-plugin eslint-plugin-react-hooks` |
| Next.js | `nextjs: true` | `@next/eslint-plugin-next` |
| Svelte | `svelte: true` | `eslint-plugin-svelte` |
| Astro | `astro: true` | `eslint-plugin-astro` |
| Solid | `solid: true` | `eslint-plugin-solid` |
| UnoCSS | `unocss: true` | `@unocss/eslint-plugin` |
## 格式化器(CSS、HTML、Markdown
针对 ESLint 本身不原生处理的文件:
```js
export default antfu({
formatters: {
css: true, // 用于 CSS/LESS/SCSS 的 Prettier
html: true, // 用于 HTML 的 Prettier
markdown: 'prettier' // 或 'dprint'
}
})
// 需要:pnpm add -D eslint-plugin-format
```
## 规则覆盖
### 全局
```js
export default antfu(
{ /* 配置选项 */ },
{ rules: { 'style/semi': ['error', 'never'] } }
)
```
### 按集成模块
```js
export default antfu({
vue: { overrides: { 'vue/operator-linebreak': ['error', 'before'] } },
typescript: { overrides: { 'ts/consistent-type-definitions': ['error', 'interface'] } },
})
```
## 插件前缀重命名
| 新前缀 | 原始名称 |
| ----------- | ----------------------- |
| `ts/*` | `@typescript-eslint/*` |
| `style/*` | `@stylistic/*` |
| `import/*` | `import-lite/*` |
| `node/*` | `n/*` |
| `test/*` | `vitest/*` |
```ts
// eslint-disable-next-line ts/consistent-type-definitions
```
## 类型感知规则
```js
export default antfu({
typescript: { tsconfigPath: 'tsconfig.json' },
})
```
## VS Code 设置
```jsonc
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "never" },
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true }
],
"eslint.validate": ["javascript", "typescript", "vue", "html", "markdown", "json", "yaml"]
}
```
+154
View File
@@ -0,0 +1,154 @@
# 包导出
## 基本单入口
```json
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"sideEffects": false,
"files": ["dist"]
}
```
## 多入口
```json
{
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.mts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
"./*": "./dist/*"
}
}
```
## 插件入口模式(unplugin-\*
```json
{
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./vite": {
"types": "./dist/vite.d.mts",
"import": "./dist/vite.mjs",
"require": "./dist/vite.cjs"
},
"./webpack": {
"types": "./dist/webpack.d.mts",
"import": "./dist/webpack.mjs",
"require": "./dist/webpack.cjs"
},
"./nuxt": {
"types": "./dist/nuxt.d.mts",
"import": "./dist/nuxt.mjs",
"require": "./dist/nuxt.cjs"
}
}
}
```
## 环境感知导出
```json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"node": {
"import": { "production": "./dist/index.prod.mjs", "development": "./dist/index.mjs" },
"require": { "production": "./dist/index.prod.cjs", "development": "./dist/index.cjs" }
},
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
```
## typesVersions 回退
针对不支持 exports 的旧版 TypeScript
```json
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./*"]
}
}
}
```
## 字段参考
| 字段 | 用途 |
| -------------- | ----------------------------------- |
| `exports` | 现代入口(Node 12.7+ |
| `main` | 旧版打包工具的 CJS 回退 |
| `module` | 打包工具的 ESM 回退 |
| `types` | TypeScript 回退 |
| `sideEffects` | `false` 启用 tree-shaking |
| `files` | 发布到 npm 的文件列表 |
## 条件顺序
顺序很重要!最具体的写在最前面:
```json
{
".": {
"types": "...", // 始终放在第一位
"import": "...", // ESM
"require": "..." // CJS 回退
}
}
```
## 对等依赖
需要消费者自行提供的外部依赖:
```json
{
"peerDependencies": {
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"vue": { "optional": true }
}
}
```
## 包验证
```bash
# 检查导出配置是否正确
pnpm dlx publint
pnpm dlx @arethetypeswrong/cli
```
加入 CI 以持续验证。
+157
View File
@@ -0,0 +1,157 @@
# 项目设置
## 单一包
```bash
# 克隆启动模板
cp -r ~/templates/antfu/starter-ts my-lib
cd my-lib && rm -rf .git && git init
pnpm install
```
或手动设置:
```bash
mkdir my-lib && cd my-lib
pnpm init
pnpm add -D typescript tsdown vitest eslint @antfu/eslint-config
```
### 目录结构
```
my-lib/
├── src/
│ ├── index.ts # 主入口
│ └── types.ts # 类型定义
├── test/
│ └── index.test.ts
├── dist/ # 构建输出(已 gitignore
├── package.json
├── tsconfig.json
├── tsdown.config.ts
├── eslint.config.ts
└── vitest.config.ts
```
## 单体仓库
```bash
cp -r ~/templates/antfu/starter-monorepo my-monorepo
cd my-monorepo && rm -rf .git && git init
pnpm install
```
### 结构
```
my-monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsdown.config.ts
│ └── cli/
│ ├── src/
│ └── package.json
├── playground/ # 集成测试
├── pnpm-workspace.yaml
├── package.json # 根脚本、devDeps
├── tsconfig.json # 基础配置
└── eslint.config.ts
```
### pnpm-workspace.yaml
```yaml
packages:
- packages/*
- playground
catalogs:
build:
tsdown: ^0.15.0
unbuild: ^3.0.0
lint:
eslint: ^9.0.0
'@antfu/eslint-config': ^4.0.0
test:
vitest: ^3.0.0
types:
typescript: ^5.7.0
```
## pnpm 目录(Catalogs
按用途组织依赖(来自 antfu 的博客文章):
| 类别 | 内容 |
| ------- | --------------------------------- |
| build | tsdown, unbuild, rollup 插件 |
| lint | eslint, @antfu/eslint-config |
| test | vitest, @vue/test-utils |
| types | typescript, @types/\* |
| prod | 运行时依赖:consola, defu, pathe |
### 使用目录
```json
{
"devDependencies": {
"tsdown": "catalog:build",
"eslint": "catalog:lint",
"vitest": "catalog:test",
"typescript": "catalog:types"
}
}
```
## ESLint 设置
```bash
pnpm add -D eslint @antfu/eslint-config
```
```typescript
// eslint.config.ts
import antfu from '@antfu/eslint-config'
export default antfu({
type: 'lib',
pnpm: true,
formatters: true,
})
```
## Git 钩子
```bash
pnpm add -D simple-git-hooks lint-staged
```
```json
{
"simple-git-hooks": { "pre-commit": "pnpm lint-staged" },
"lint-staged": { "*": "eslint --fix" },
"scripts": { "prepare": "simple-git-hooks" }
}
```
添加后执行 `pnpm prepare`
## 脚本
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit",
"test": "vitest",
"release": "bumpp",
"prepublishOnly": "pnpm build"
}
}
```
+180
View File
@@ -0,0 +1,180 @@
# 发布工作流
## 工具
| 工具 | 用途 |
| ----------- | --------------------------------- |
| bumpp | 交互式版本号递增 |
| changelogen | 从提交记录生成变更日志 |
| pkg-pr-new | PR 预览包 |
## bumpp(版本号递增)
```bash
pnpm add -D bumpp
```
```json
{
"scripts": {
"release": "bumpp"
}
}
```
交互式提示选择补丁/次要/主要版本。选项:
```json
{
"scripts": {
"release": "bumpp --commit --tag --push"
}
}
```
对于单体仓库:
```bash
bumpp -r # 递归模式
bumpp packages/*/package.json # 指定包
```
## changelogen(变更日志)
```bash
pnpm add -D changelogen
```
```json
{
"scripts": {
"changelog": "changelogen --release"
}
}
```
组合工作流:
```json
{
"scripts": {
"release": "changelogen --release && bumpp"
}
}
```
## 完整发布流程
```json
{
"scripts": {
"release": "pnpm lint && pnpm test && changelogen --release && bumpp --commit --tag --push"
}
}
```
CI 在标签推送时发布到 npm。
## pkg-pr-newPR 预览)
适用于可发布的包。在 PR 上创建安装链接。
```yaml
# .github/workflows/pkg-pr-new.yml
name: Publish PR
on: pull_request
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm build
- run: pnpm dlx pkg-pr-new publish --compact --pnpm
```
对于单体仓库:
```bash
pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*'
```
PR 评论显示:
```
pnpm add https://pkg.pr.new/your-org/your-package@123
```
## 约定式提交
为了让 changelogen 正常工作:
```
feat: add dark mode support
fix: resolve memory leak in parser
docs: update README
chore: update dependencies
```
## npm 发布
### 基于 Token(传统方式)
```yaml
- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### OIDC(推荐方式)
无需 Token。完整配置请参见 ci-workflows.md。
```yaml
- run: pnpm publish --access public --no-git-checks --provenance
```
## 单体仓库发布
使用 pnpm
```bash
pnpm -r publish --access public
```
使用 bumpp
```bash
bumpp -r && pnpm -r publish
```
## 预发布版本
```bash
bumpp --preid beta # 1.0.0 -> 1.0.1-beta.0
bumpp --preid alpha # 1.0.0 -> 1.0.1-alpha.0
```
## Package.json 要求
```json
{
"name": "@scope/package",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/org/repo.git"
},
"publishConfig": {
"access": "public"
}
}
```
`repository` 字段对于 npm provenance 是必需的。
+201
View File
@@ -0,0 +1,201 @@
# 测试
## Vitest 配置
```bash
pnpm add -D vitest
```
### 基础配置
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
testTimeout: 30_000,
reporters: 'dot',
},
})
```
### 带覆盖率配置
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/types.ts'],
reporter: ['text', 'lcovonly', 'html'],
},
},
})
```
## 工作空间项目
对于单体仓库(monorepo),分别测试各个包:
```typescript
export default defineConfig({
test: {
projects: [
'packages/*/vitest.config.ts',
{
extends: './vitest.config.ts',
test: { name: 'unit', environment: 'node' },
},
{
extends: './vitest.config.ts',
test: { name: 'browser', browser: { enabled: true } },
},
],
},
})
```
## 基于 Fixture 的测试
使用文件 fixture 测试转换:
```typescript
import { describe, expect, it } from 'vitest'
import { transform } from '../src'
const fixtures = import.meta.glob('./fixtures/*.ts', { as: 'raw' })
describe('transform', () => {
for (const [path, getContent] of Object.entries(fixtures)) {
it(path, async () => {
const content = await getContent()
const result = await transform(content)
expect(result).toMatchSnapshot()
})
}
})
```
## 幂等性测试
确保转换是稳定的:
```typescript
it('transform is idempotent', async () => {
const pass1 = (await transform(fixture))?.code ?? fixture
expect(pass1).toMatchSnapshot()
const pass2 = (await transform(pass1))?.code ?? pass1
expect(pass2).toBe(pass1) // 不应发生改变
})
```
## 类型级测试
测试 TypeScript 类型:
```typescript
// vitest.config.ts
export default defineConfig({
test: {
typecheck: { enabled: true },
},
})
```
```typescript
// test/types.test-d.ts
import { describe, expectTypeOf, it } from 'vitest'
import type { Input, Output } from '../src'
describe('types', () => {
it('infers input correctly', () => {
expectTypeOf<Input<typeof schema>>().toEqualTypeOf<{ id: string }>()
})
})
```
## 多 TypeScript 版本测试
跨 TypeScript 版本进行测试(TanStack 模式):
```yaml
# .github/workflows/ci.yml
jobs:
test-types:
strategy:
matrix:
ts: ['5.0', '5.2', '5.4', '5.6', '5.8']
steps:
- run: pnpm add -D typescript@${{ matrix.ts }}
- run: pnpm typecheck
```
## 包验证
验证已发布的包:
```bash
# 检查导出是否正确
pnpm dlx publint
# 检查类型在不同 moduleResolution 下是否正常
pnpm dlx @arethetypeswrong/cli --pack .
```
添加到 tsdown 配置中:
```typescript
export default defineConfig({
attw: { profile: 'esm-only' }, // 或 'node16'
})
```
## 测试脚本
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:types": "vitest typecheck"
}
}
```
## Mock 模拟
```typescript
import { vi } from 'vitest'
vi.mock('fs', () => ({
readFileSync: vi.fn(() => 'mocked content'),
}))
// 监听方法
const spy = vi.spyOn(console, 'log')
expect(spy).toHaveBeenCalledWith('expected')
```
## 测试插件
在测试中自用(Dogfood)自己的插件:
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import MyPlugin from './src/vite'
export default defineConfig({
plugins: [
MyPlugin({ /* options */ }),
],
test: {
include: ['test/**/*.test.ts'],
},
})
```
+191
View File
@@ -0,0 +1,191 @@
# 类型模式
## 工具类型
各类库中常用的辅助类型:
```typescript
// Promise 或同步
export type Awaitable<T> = T | Promise<T>
// 单个或数组
export type Arrayable<T> = T | T[]
// 可空
export type Nullable<T> = T | null | undefined
// 深度 Partial
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// 简化交叉类型以优化 IDE 显示
export type Simplify<T> = { [K in keyof T]: T[K] } & {}
// 阻止特定位置的类型推断
export type NoInfer<T> = [T][T extends any ? 0 : never]
```
## 条件提取
从结构中提取类型:
```typescript
// 从 schema 中提取输入类型
export type Input<T> = T extends { _input: infer U } ? U : unknown
// 提取输出类型
export type Output<T> = T extends { _output: infer U } ? U : unknown
// 从嵌套属性中提取
export type InferContext<T> = T extends { context: infer C } ? C : never
```
## 品牌类型
基本类型的名义类型:
```typescript
declare const brand: unique symbol
export type Brand<T, B> = T & { readonly [brand]: B }
export type UserId = Brand<string, 'UserId'>
export type PostId = Brand<string, 'PostId'>
// 无法混用
function getUser(id: UserId) { /* ... */ }
getUser('abc' as UserId) // 正确
getUser('abc' as PostId) // 错误!
```
## 类型累积(构建器模式)
每个方法更新泛型参数:
```typescript
interface ProcedureBuilder<TContext, TInput, TOutput> {
input<T>(schema: T): ProcedureBuilder<TContext, T, TOutput>
output<T>(schema: T): ProcedureBuilder<TContext, TInput, T>
query(fn: (opts: { ctx: TContext; input: TInput }) => TOutput): Procedure
}
// 类型在链中传递
const proc = builder
.input(z.object({ id: z.string() })) // TInput = { id: string }
.output(z.object({ name: z.string() })) // TOutput = { name: string }
.query(({ input }) => ({ name: input.id }))
```
## 模块扩充
允许用户扩展库类型:
```typescript
// 库代码
export interface Register {}
export type DefaultError = Register extends { defaultError: infer E }
? E
: Error
// 用户代码
declare module 'my-lib' {
interface Register {
defaultError: MyCustomError
}
}
```
## 数据标记
使用 symbol 附加类型元数据:
```typescript
declare const dataTagSymbol: unique symbol
declare const errorTagSymbol: unique symbol
export type DataTag<TType, TData, TError> = TType & {
[dataTagSymbol]: TData
[errorTagSymbol]: TError
}
// 提取标记类型
export type InferData<T> = T extends { [dataTagSymbol]: infer D } ? D : unknown
```
## 映射类型修改
列构建器模式(drizzle):
```typescript
type NotNull<T extends ColumnBuilder> = T & { _: { notNull: true } }
type HasDefault<T extends ColumnBuilder> = T & { _: { hasDefault: true } }
class ColumnBuilder<T extends ColumnConfig> {
notNull(): NotNull<this> {
// ...
return this as NotNull<this>
}
default(value: T['data']): HasDefault<this> {
// ...
return this as HasDefault<this>
}
}
```
## 编译时错误
返回可读的错误信息:
```typescript
type TypeError<Message extends string> = { __error: Message }
type ValidateInput<T> = T extends string
? T
: TypeError<'Input must be a string'>
// 显示:类型 'TypeError<"Input must be a string">' 不可赋值给...
```
## 函数重载
针对不同输入的多重签名:
```typescript
export function useEventListener<E extends keyof WindowEventMap>(
event: E,
listener: (ev: WindowEventMap[E]) => any
): void
export function useEventListener<E extends keyof DocumentEventMap>(
target: Document,
event: E,
listener: (ev: DocumentEventMap[E]) => any
): void
export function useEventListener(...args: any[]) {
// 实现
}
```
## 分发条件类型
应用于联合类型的每个成员:
```typescript
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// Result = string[] | number[]
```
使用元组禁用分发:
```typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result = ToArrayNonDist<string | number>
// Result = (string | number)[]
```
+144
View File
@@ -0,0 +1,144 @@
# TypeScript 配置
## 库基础配置
```json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext"],
"strict": true,
"strictNullChecks": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedDeclarations": true,
"verbatimModuleSyntax": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
```
## 关键选项说明
| 选项 | 值 | 原因 |
| ---------------------- | ------- | -------------------------------------------------- |
| `target` | ESNext | 现代输出,由打包工具降级 |
| `module` | ESNext | ESM 输出 |
| `moduleResolution` | Bundler | 与现代打包工具协作,允许省略扩展名 |
| `strict` | true | 尽早捕获错误 |
| `noEmit` | true | 构建工具负责输出 |
| `isolatedDeclarations` | true | 更快的 DTS 生成 |
| `verbatimModuleSyntax` | true | 需要显式使用 `import type` |
| `skipLibCheck` | true | 加快构建速度 |
## 单体仓库配置
### 根目录 tsconfig.json
```json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true
}
}
```
### 包 tsconfig.json
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [
{ "path": "../utils" }
]
}
```
## 路径别名
用于单体仓库的内部导入:
```json
{
"compilerOptions": {
"paths": {
"@my-lib/core": ["./packages/core/src"],
"@my-lib/utils": ["./packages/utils/src"],
"#internal/*": ["./virtual-shared/*"]
}
}
}
```
## Bundler 与 Node 解析模式对比
**对由打包工具(Vite、webpack 等)消费的库,使用 `Bundler`:**
- 允许导入时省略扩展名
- 支持 package.json 中的 `exports` 字段
- 现代、更简单的配置
**对仅用于 Node.js 的库,使用 `Node16/NodeNext`**
- 需要显式扩展名(`.js`
- 更严格,与 Node.js 行为完全一致
## 类型声明
让构建工具生成声明:
```typescript
// tsdown.config.ts
export default defineConfig({
dts: true, // 生成 .d.ts
dts: { resolve: ['@antfu/utils'] } // 内联特定类型
})
```
或配合 unbuild
```typescript
// build.config.ts
export default defineBuildConfig({
declaration: 'node16', // 为 Node.js 兼容性
declaration: true, // 为打包工具解析
})
```
## 常见问题
### 模块未找到错误
检查 `moduleResolution` 是否与目标环境匹配:
- 打包工具:`"Bundler"`
- Node.js`"Node16"``"NodeNext"`
### 类型导入不起作用
启用 `verbatimModuleSyntax` 并使用显式导入:
```typescript
import type { Foo } from './types'
```
### 类型检查缓慢
启用 `skipLibCheck: true``isolatedDeclarations: true`
+154
View File
@@ -0,0 +1,154 @@
# Package Exports(包导出)
## Basic Single Entry(基本单入口)
```json
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"sideEffects": false,
"files": ["dist"]
}
```
## Multiple Entry Points(多入口点)
```json
{
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.mts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
"./*": "./dist/*"
}
}
```
## Plugin Entry Pattern (unplugin-\*)(插件入口模式,unplugin-\*
```json
{
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./vite": {
"types": "./dist/vite.d.mts",
"import": "./dist/vite.mjs",
"require": "./dist/vite.cjs"
},
"./webpack": {
"types": "./dist/webpack.d.mts",
"import": "./dist/webpack.mjs",
"require": "./dist/webpack.cjs"
},
"./nuxt": {
"types": "./dist/nuxt.d.mts",
"import": "./dist/nuxt.mjs",
"require": "./dist/nuxt.cjs"
}
}
}
```
## Environment-Aware Exports(环境感知导出)
```json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"node": {
"import": { "production": "./dist/index.prod.mjs", "development": "./dist/index.mjs" },
"require": { "production": "./dist/index.prod.cjs", "development": "./dist/index.cjs" }
},
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
```
## typesVersions FallbacktypesVersions 回退)
针对不支持 exports 的旧版 TypeScript
```json
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./*"]
}
}
}
```
## Field Reference(字段参考)
| 字段 | 用途 |
| -------------- | -------------------------------------- |
| `exports` | 现代入口点(Node 12.7+ |
| `main` | 针对旧版打包工具的 CJS 回退 |
| `module` | 针对打包工具的 ESM 回退 |
| `types` | TypeScript 回退 |
| `sideEffects` | `false` 启用 tree-shaking |
| `files` | 发布到 npm 的文件列表 |
## Condition Order(条件顺序)
顺序很重要!最具体的放在最前面:
```json
{
".": {
"types": "...", // 始终放在第一位
"import": "...", // ESM
"require": "..." // CJS 回退
}
}
```
## Peer Dependencies(对等依赖)
消费者必须提供的外部依赖:
```json
{
"peerDependencies": {
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"vue": { "optional": true }
}
}
```
## Package Validation(包验证)
```bash
# 检查导出配置是否正确
pnpm dlx publint
pnpm dlx @arethetypeswrong/cli
```
将以上步骤加入 CI 以实现持续验证。
+157
View File
@@ -0,0 +1,157 @@
# 项目配置
## 单包
```bash
# 克隆启动模板
cp -r ~/templates/antfu/starter-ts my-lib
cd my-lib && rm -rf .git && git init
pnpm install
```
或者手动配置:
```bash
mkdir my-lib && cd my-lib
pnpm init
pnpm add -D typescript tsdown vitest eslint @antfu/eslint-config
```
### 目录结构
```
my-lib/
├── src/
│ ├── index.ts # 主入口
│ └── types.ts # 类型定义
├── test/
│ └── index.test.ts
├── dist/ # 构建输出(已 gitignore
├── package.json
├── tsconfig.json
├── tsdown.config.ts
├── eslint.config.ts
└── vitest.config.ts
```
## 单体仓库
```bash
cp -r ~/templates/antfu/starter-monorepo my-monorepo
cd my-monorepo && rm -rf .git && git init
pnpm install
```
### 结构
```
my-monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsdown.config.ts
│ └── cli/
│ ├── src/
│ └── package.json
├── playground/ # 集成测试
├── pnpm-workspace.yaml
├── package.json # 根级脚本、devDeps
├── tsconfig.json # 基础配置
└── eslint.config.ts
```
### pnpm-workspace.yaml
```yaml
packages:
- packages/*
- playground
catalogs:
build:
tsdown: ^0.15.0
unbuild: ^3.0.0
lint:
eslint: ^9.0.0
'@antfu/eslint-config': ^4.0.0
test:
vitest: ^3.0.0
types:
typescript: ^5.7.0
```
## pnpm Catalogs
按用途组织依赖项(来自 antfu 的博客文章):
| 类别 | 内容 |
| ------- | ---------------------------------- |
| build | tsdown, unbuild, rollup 插件 |
| lint | eslint, @antfu/eslint-config |
| test | vitest, @vue/test-utils |
| types | typescript, @types/\* |
| prod | 运行时依赖:consola, defu, pathe |
### 使用 Catalogs
```json
{
"devDependencies": {
"tsdown": "catalog:build",
"eslint": "catalog:lint",
"vitest": "catalog:test",
"typescript": "catalog:types"
}
}
```
## ESLint 配置
```bash
pnpm add -D eslint @antfu/eslint-config
```
```typescript
// eslint.config.ts
import antfu from '@antfu/eslint-config'
export default antfu({
type: 'lib',
pnpm: true,
formatters: true,
})
```
## Git Hooks
```bash
pnpm add -D simple-git-hooks lint-staged
```
```json
{
"simple-git-hooks": { "pre-commit": "pnpm lint-staged" },
"lint-staged": { "*": "eslint --fix" },
"scripts": { "prepare": "simple-git-hooks" }
}
```
添加后运行 `pnpm prepare`
## 脚本
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit",
"test": "vitest",
"release": "bumpp",
"prepublishOnly": "pnpm build"
}
}
```
+144
View File
@@ -0,0 +1,144 @@
# TypeScript 配置
## 库基础配置
```json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ESNext"],
"strict": true,
"strictNullChecks": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedDeclarations": true,
"verbatimModuleSyntax": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
```
## 关键选项说明
| 选项 | 值 | 说明 |
| ------------------------ | -------- | --------------------------------------------------- |
| `target` | ESNext | 现代输出,由打包工具向下兼容 |
| `module` | ESNext | ESM 输出 |
| `moduleResolution` | Bundler | 兼容现代打包工具,允许省略扩展名 |
| `strict` | true | 尽早捕获错误 |
| `noEmit` | true | 由构建工具处理输出 |
| `isolatedDeclarations` | true | 更快的 DTS 生成 |
| `verbatimModuleSyntax` | true | 要求显式使用 `import type` |
| `skipLibCheck` | true | 加快构建速度 |
## 单体仓库配置
### 根目录 tsconfig.json
```json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true
}
}
```
### 子包 tsconfig.json
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [
{ "path": "../utils" }
]
}
```
## 路径别名
用于单体仓库的内部导入:
```json
{
"compilerOptions": {
"paths": {
"@my-lib/core": ["./packages/core/src"],
"@my-lib/utils": ["./packages/utils/src"],
"#internal/*": ["./virtual-shared/*"]
}
}
}
```
## Bundler 与 Node 解析模式对比
**对由打包工具(Vite、webpack 等)消费的库,使用 `Bundler`**
- 允许导入时不加扩展名
- 支持 package.json 中的 `exports` 字段
- 现代、更简单的配置
**对仅用于 Node.js 的库,使用 `Node16/NodeNext`**
- 需要显式扩展名(`.js`
- 更严格,与 Node.js 行为完全一致
## 类型声明
让构建工具生成声明文件:
```typescript
// tsdown.config.ts
export default defineConfig({
dts: true, // 生成 .d.ts
dts: { resolve: ['@antfu/utils'] } // 内联特定类型
})
```
或使用 unbuild
```typescript
// build.config.ts
export default defineBuildConfig({
declaration: 'node16', // 为 Node.js 兼容性
declaration: true, // 为打包工具解析模式
})
```
## 常见问题
### 模块未找到错误
检查 `moduleResolution` 是否与目标环境匹配:
- 打包工具:`"Bundler"`
- Node.js`"Node16"``"NodeNext"`
### 类型导入不生效
启用 `verbatimModuleSyntax` 并使用显式写法:
```typescript
import type { Foo } from './types'
```
### 类型检查缓慢
启用 `skipLibCheck: true``isolatedDeclarations: true`
+9
View File
@@ -0,0 +1,9 @@
- 逐句精准翻译,未改变语意或增删信息
- 保留完整 Markdown 结构与 YAML frontmatter 结构不变
- 键名保持英文原样,仅翻译英文散文值
- 代码块、命令、路径、URL、环境变量等原样保留
- 英文专有名词保留原文
- 中文标点(句号、逗号),代码/URL 内标点不动
- 无多余解释或包裹层
有任何需要调整的地方请告诉我。
+180
View File
@@ -0,0 +1,180 @@
# 发布工作流
## 工具
| 工具 | 用途 |
| ------------- | ------------------------------ |
| bumpp | 交互式版本号递增 |
| changelogen | 从提交记录生成变更日志 |
| pkg-pr-new | PR 预览包 |
## bumpp(版本号递增)
```bash
pnpm add -D bumpp
```
```json
{
"scripts": {
"release": "bumpp"
}
}
```
交互式提示选择补丁/次版本/主版本。可选参数:
```json
{
"scripts": {
"release": "bumpp --commit --tag --push"
}
}
```
针对单体仓库:
```bash
bumpp -r # 递归
bumpp packages/*/package.json # 指定包
```
## changelogen(变更日志)
```bash
pnpm add -D changelogen
```
```json
{
"scripts": {
"changelog": "changelogen --release"
}
}
```
组合工作流:
```json
{
"scripts": {
"release": "changelogen --release && bumpp"
}
}
```
## 完整发布流程
```json
{
"scripts": {
"release": "pnpm lint && pnpm test && changelogen --release && bumpp --commit --tag --push"
}
}
```
CI 在标签推送时发布到 npm。
## pkg-pr-newPR 预览)
适用于可发布的包。在 PR 上创建安装链接。
```yaml
# .github/workflows/pkg-pr-new.yml
name: Publish PR
on: pull_request
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install
- run: pnpm build
- run: pnpm dlx pkg-pr-new publish --compact --pnpm
```
针对单体仓库:
```bash
pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*'
```
PR 评论会显示:
```
pnpm add https://pkg.pr.new/your-org/your-package@123
```
## 约定式提交
changelogen 正常工作需要:
```
feat: add dark mode support
fix: resolve memory leak in parser
docs: update README
chore: update dependencies
```
## npm 发布
### 基于令牌(旧方式)
```yaml
- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### OIDC(推荐)
无需令牌。完整配置见 ci-workflows.md。
```yaml
- run: pnpm publish --access public --no-git-checks --provenance
```
## 单体仓库发布
使用 pnpm
```bash
pnpm -r publish --access public
```
使用 bumpp
```bash
bumpp -r && pnpm -r publish
```
## 预发布版本
```bash
bumpp --preid beta # 1.0.0 -> 1.0.1-beta.0
bumpp --preid alpha # 1.0.0 -> 1.0.1-alpha.0
```
## Package.json 要求
```json
{
"name": "@scope/package",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/org/repo.git"
},
"publishConfig": {
"access": "public"
}
}
```
`repository` 对于 npm provenance 是必需的。
+201
View File
@@ -0,0 +1,201 @@
# 测试
## Vitest 配置
```bash
pnpm add -D vitest
```
### 基础配置
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
testTimeout: 30_000,
reporters: 'dot',
},
})
```
### 含覆盖率
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/types.ts'],
reporter: ['text', 'lcovonly', 'html'],
},
},
})
```
## 工作空间项目
对于 monorepo,可以分别测试各个包:
```typescript
export default defineConfig({
test: {
projects: [
'packages/*/vitest.config.ts',
{
extends: './vitest.config.ts',
test: { name: 'unit', environment: 'node' },
},
{
extends: './vitest.config.ts',
test: { name: 'browser', browser: { enabled: true } },
},
],
},
})
```
## 基于 Fixture 的测试
使用文件 fixture 测试转换:
```typescript
import { describe, expect, it } from 'vitest'
import { transform } from '../src'
const fixtures = import.meta.glob('./fixtures/*.ts', { as: 'raw' })
describe('transform', () => {
for (const [path, getContent] of Object.entries(fixtures)) {
it(path, async () => {
const content = await getContent()
const result = await transform(content)
expect(result).toMatchSnapshot()
})
}
})
```
## 幂等性测试
确保转换是稳定的:
```typescript
it('transform is idempotent', async () => {
const pass1 = (await transform(fixture))?.code ?? fixture
expect(pass1).toMatchSnapshot()
const pass2 = (await transform(pass1))?.code ?? pass1
expect(pass2).toBe(pass1) // 不应发生变化
})
```
## 类型级测试
测试 TypeScript 类型:
```typescript
// vitest.config.ts
export default defineConfig({
test: {
typecheck: { enabled: true },
},
})
```
```typescript
// test/types.test-d.ts
import { describe, expectTypeOf, it } from 'vitest'
import type { Input, Output } from '../src'
describe('types', () => {
it('infers input correctly', () => {
expectTypeOf<Input<typeof schema>>().toEqualTypeOf<{ id: string }>()
})
})
```
## 多 TS 版本测试
跨 TypeScript 版本测试(TanStack 模式):
```yaml
# .github/workflows/ci.yml
jobs:
test-types:
strategy:
matrix:
ts: ['5.0', '5.2', '5.4', '5.6', '5.8']
steps:
- run: pnpm add -D typescript@${{ matrix.ts }}
- run: pnpm typecheck
```
## 包验证
验证发布后的包:
```bash
# 检查导出是否正确
pnpm dlx publint
# 检查类型在不同 moduleResolution 下是否正常工作
pnpm dlx @arethetypeswrong/cli --pack .
```
添加到 tsdown 配置:
```typescript
export default defineConfig({
attw: { profile: 'esm-only' }, // 或 'node16'
})
```
## 测试脚本
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:types": "vitest typecheck"
}
}
```
## Mock
```typescript
import { vi } from 'vitest'
vi.mock('fs', () => ({
readFileSync: vi.fn(() => 'mocked content'),
}))
// 监听方法
const spy = vi.spyOn(console, 'log')
expect(spy).toHaveBeenCalledWith('expected')
```
## 测试插件
在测试中自己使用自己的插件:
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import MyPlugin from './src/vite'
export default defineConfig({
plugins: [
MyPlugin({ /* 选项 */ }),
],
test: {
include: ['test/**/*.test.ts'],
},
})
```