chore: import zh skill ts-library
This commit is contained in:
@@ -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 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -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)
|
||||
- 设计类型安全的 API(builder、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,每个参考文件约 800–1200 token_
|
||||
@@ -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
@@ -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
|
||||
```
|
||||
|
||||
## 惰性 Getter(Tree-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'
|
||||
```
|
||||
@@ -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)[]
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
|
||||
## 惰性 Getter(Tree-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'
|
||||
```
|
||||
@@ -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`。
|
||||
@@ -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 .
|
||||
```
|
||||
@@ -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"]
|
||||
}
|
||||
```
|
||||
@@ -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 以持续验证。
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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-new(PR 预览)
|
||||
|
||||
适用于可发布的包。在 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 是必需的。
|
||||
@@ -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'],
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -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)[]
|
||||
```
|
||||
@@ -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`。
|
||||
@@ -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 Fallback(typesVersions 回退)
|
||||
|
||||
针对不支持 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 以实现持续验证。
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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`。
|
||||
@@ -0,0 +1,9 @@
|
||||
- 逐句精准翻译,未改变语意或增删信息
|
||||
- 保留完整 Markdown 结构与 YAML frontmatter 结构不变
|
||||
- 键名保持英文原样,仅翻译英文散文值
|
||||
- 代码块、命令、路径、URL、环境变量等原样保留
|
||||
- 英文专有名词保留原文
|
||||
- 中文标点(句号、逗号),代码/URL 内标点不动
|
||||
- 无多余解释或包裹层
|
||||
|
||||
有任何需要调整的地方请告诉我。
|
||||
@@ -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-new(PR 预览)
|
||||
|
||||
适用于可发布的包。在 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 是必需的。
|
||||
@@ -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'],
|
||||
},
|
||||
})
|
||||
```
|
||||
Reference in New Issue
Block a user