From 8dd69a3f6c8ef66373bf5c6abaa349608bebc427 Mon Sep 17 00:00:00 2001 From: wehub-skill-sync Date: Mon, 13 Jul 2026 21:37:05 +0800 Subject: [PATCH] chore: import zh skill ts-library --- README.wehub.md | 9 ++ SKILL.md | 105 +++++++++++++ build/tooling.md | 187 ++++++++++++++++++++++ patterns/api.md | 210 +++++++++++++++++++++++++ patterns/types.md | 191 +++++++++++++++++++++++ references/api-design.md | 210 +++++++++++++++++++++++++ references/build-tooling.md | 187 ++++++++++++++++++++++ references/ci-workflows.md | 265 ++++++++++++++++++++++++++++++++ references/eslint-config.md | 120 +++++++++++++++ references/package-exports.md | 154 +++++++++++++++++++ references/project-setup.md | 157 +++++++++++++++++++ references/release.md | 180 ++++++++++++++++++++++ references/testing.md | 201 ++++++++++++++++++++++++ references/type-patterns.md | 191 +++++++++++++++++++++++ references/typescript-config.md | 144 +++++++++++++++++ setup/exports.md | 154 +++++++++++++++++++ setup/project.md | 157 +++++++++++++++++++ setup/tsconfig.md | 144 +++++++++++++++++ workflows/ci.md | 9 ++ workflows/release.md | 180 ++++++++++++++++++++++ workflows/testing.md | 201 ++++++++++++++++++++++++ 21 files changed, 3356 insertions(+) create mode 100644 README.wehub.md create mode 100644 SKILL.md create mode 100644 build/tooling.md create mode 100644 patterns/api.md create mode 100644 patterns/types.md create mode 100644 references/api-design.md create mode 100644 references/build-tooling.md create mode 100644 references/ci-workflows.md create mode 100644 references/eslint-config.md create mode 100644 references/package-exports.md create mode 100644 references/project-setup.md create mode 100644 references/release.md create mode 100644 references/testing.md create mode 100644 references/type-patterns.md create mode 100644 references/typescript-config.md create mode 100644 setup/exports.md create mode 100644 setup/project.md create mode 100644 setup/tsconfig.md create mode 100644 workflows/ci.md create mode 100644 workflows/release.md create mode 100644 workflows/testing.md diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..7721002 --- /dev/null +++ b/README.wehub.md @@ -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 清单整理 +- 原作者、版权和许可证信息以上游仓库为准 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..3112fbc --- /dev/null +++ b/SKILL.md @@ -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_ diff --git a/build/tooling.md b/build/tooling.md new file mode 100644 index 0000000..934abf3 --- /dev/null +++ b/build/tooling.md @@ -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` 中列出。 diff --git a/patterns/api.md b/patterns/api.md new file mode 100644 index 0000000..6c9f46c --- /dev/null +++ b/patterns/api.md @@ -0,0 +1,210 @@ +# API 设计模式 + +## Options 模式 + +面向用户的选项及其内部解析后的版本: + +```typescript +export interface Options { + verbose?: boolean + include?: string[] + exclude?: string[] +} + +export interface ResolvedOptions extends Required { + 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() { + return { + context(): Builder { + return this as any + }, + input(schema: T): Builder { + return this as any + }, + output(schema: T): Builder { + return this as any + }, + build(): Procedure { /* ... */ }, + } +} + +// 用法 - 类型在链中传递 +const procedure = createBuilder() + .context<{ user: User }>() + .input(z.object({ id: z.string() })) + .build() +``` + +## 插件模式(unplugin) + +从单一实现生成通用插件: + +```typescript +import { createUnplugin } from 'unplugin' + +export default createUnplugin((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(factory: PluginFactory) { + 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 { + private _def: SchemaDef + + min(value: number): Schema { + return new Schema({ ...this._def, min: value }) + } + + max(value: number): Schema { + return new Schema({ ...this._def, max: value }) + } + + optional(): Schema { + 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' +``` diff --git a/patterns/types.md b/patterns/types.md new file mode 100644 index 0000000..de5f17d --- /dev/null +++ b/patterns/types.md @@ -0,0 +1,191 @@ +# 类型模式 + +## 工具类型 + +各库中常用的辅助类型: + +```typescript +// Promise 或同步 +export type Awaitable = T | Promise + +// 单个或数组 +export type Arrayable = T | T[] + +// 可空 +export type Nullable = T | null | undefined + +// 深度可选 +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] +} + +// 简化交叉类型以改善 IDE 显示 +export type Simplify = { [K in keyof T]: T[K] } & {} + +// 阻止特定位置的类型推断 +export type NoInfer = [T][T extends any ? 0 : never] +``` + +## 条件提取 + +从结构中提取类型: + +```typescript +// 从 schema 中提取输入类型 +export type Input = T extends { _input: infer U } ? U : unknown + +// 提取输出类型 +export type Output = T extends { _output: infer U } ? U : unknown + +// 从嵌套属性中提取 +export type InferContext = T extends { context: infer C } ? C : never +``` + +## 品牌类型 + +为原始类型添加名义类型: + +```typescript +declare const brand: unique symbol + +export type Brand = T & { readonly [brand]: B } + +export type UserId = Brand +export type PostId = Brand + +// 不会混淆 +function getUser(id: UserId) { /* ... */ } +getUser('abc' as UserId) // 正确 +getUser('abc' as PostId) // 错误! +``` + +## 类型累积(构建器模式) + +每个方法更新泛型参数: + +```typescript +interface ProcedureBuilder { + input(schema: T): ProcedureBuilder + output(schema: T): ProcedureBuilder + 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 & { + [dataTagSymbol]: TData + [errorTagSymbol]: TError +} + +// 提取标记类型 +export type InferData = T extends { [dataTagSymbol]: infer D } ? D : unknown +``` + +## 映射类型修饰 + +列构建器模式(drizzle): + +```typescript +type NotNull = T & { _: { notNull: true } } +type HasDefault = T & { _: { hasDefault: true } } + +class ColumnBuilder { + notNull(): NotNull { + // ... + return this as NotNull + } + + default(value: T['data']): HasDefault { + // ... + return this as HasDefault + } +} +``` + +## 编译时错误 + +返回可读的错误信息: + +```typescript +type TypeError = { __error: Message } + +type ValidateInput = T extends string + ? T + : TypeError<'Input must be a string'> + +// 显示:类型 "TypeError<"Input must be a string">" 不可赋值给... +``` + +## 函数重载 + +为不同输入提供多个签名: + +```typescript +export function useEventListener( + event: E, + listener: (ev: WindowEventMap[E]) => any +): void + +export function useEventListener( + target: Document, + event: E, + listener: (ev: DocumentEventMap[E]) => any +): void + +export function useEventListener(...args: any[]) { + // 实现 +} +``` + +## 分布式条件类型 + +应用于每个联合成员: + +```typescript +type ToArray = T extends any ? T[] : never + +type Result = ToArray +// Result = string[] | number[] +``` + +使用元组禁用分发: + +```typescript +type ToArrayNonDist = [T] extends [any] ? T[] : never + +type Result = ToArrayNonDist +// Result = (string | number)[] +``` diff --git a/references/api-design.md b/references/api-design.md new file mode 100644 index 0000000..b8b0497 --- /dev/null +++ b/references/api-design.md @@ -0,0 +1,210 @@ +# API 设计模式 + +## Options 模式 + +对外暴露可配置选项,内部使用已解析的确定版本: + +```typescript +export interface Options { + verbose?: boolean + include?: string[] + exclude?: string[] +} + +export interface ResolvedOptions extends Required { + 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() { + return { + context(): Builder { + return this as any + }, + input(schema: T): Builder { + return this as any + }, + output(schema: T): Builder { + return this as any + }, + build(): Procedure { /* ... */ }, + } +} + +// 使用示例 —— 类型随链式调用流动 +const procedure = createBuilder() + .context<{ user: User }>() + .input(z.object({ id: z.string() })) + .build() +``` + +## 插件模式(unplugin) + +通过单一实现生成通用插件: + +```typescript +import { createUnplugin } from 'unplugin' + +export default createUnplugin((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(factory: PluginFactory) { + 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 { + private _def: SchemaDef + + min(value: number): Schema { + return new Schema({ ...this._def, min: value }) + } + + max(value: number): Schema { + return new Schema({ ...this._def, max: value }) + } + + optional(): Schema { + 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' +``` diff --git a/references/build-tooling.md b/references/build-tooling.md new file mode 100644 index 0000000..70fdd04 --- /dev/null +++ b/references/build-tooling.md @@ -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`。 diff --git a/references/ci-workflows.md b/references/ci-workflows.md new file mode 100644 index 0000000..c7a011f --- /dev/null +++ b/references/ci-workflows.md @@ -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//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 . +``` diff --git a/references/eslint-config.md b/references/eslint-config.md new file mode 100644 index 0000000..15edf62 --- /dev/null +++ b/references/eslint-config.md @@ -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"] +} +``` diff --git a/references/package-exports.md b/references/package-exports.md new file mode 100644 index 0000000..5e8de44 --- /dev/null +++ b/references/package-exports.md @@ -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 以持续验证。 diff --git a/references/project-setup.md b/references/project-setup.md new file mode 100644 index 0000000..ec590d1 --- /dev/null +++ b/references/project-setup.md @@ -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" + } +} +``` diff --git a/references/release.md b/references/release.md new file mode 100644 index 0000000..2783afb --- /dev/null +++ b/references/release.md @@ -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 是必需的。 diff --git a/references/testing.md b/references/testing.md new file mode 100644 index 0000000..444e015 --- /dev/null +++ b/references/testing.md @@ -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>().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'], + }, +}) +``` diff --git a/references/type-patterns.md b/references/type-patterns.md new file mode 100644 index 0000000..06580e4 --- /dev/null +++ b/references/type-patterns.md @@ -0,0 +1,191 @@ +# 类型模式 + +## 工具类型 + +各类库中常用的辅助类型: + +```typescript +// Promise 或同步 +export type Awaitable = T | Promise + +// 单个或数组 +export type Arrayable = T | T[] + +// 可空 +export type Nullable = T | null | undefined + +// 深度 Partial +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] +} + +// 简化交叉类型以优化 IDE 显示 +export type Simplify = { [K in keyof T]: T[K] } & {} + +// 阻止特定位置的类型推断 +export type NoInfer = [T][T extends any ? 0 : never] +``` + +## 条件提取 + +从结构中提取类型: + +```typescript +// 从 schema 中提取输入类型 +export type Input = T extends { _input: infer U } ? U : unknown + +// 提取输出类型 +export type Output = T extends { _output: infer U } ? U : unknown + +// 从嵌套属性中提取 +export type InferContext = T extends { context: infer C } ? C : never +``` + +## 品牌类型 + +基本类型的名义类型: + +```typescript +declare const brand: unique symbol + +export type Brand = T & { readonly [brand]: B } + +export type UserId = Brand +export type PostId = Brand + +// 无法混用 +function getUser(id: UserId) { /* ... */ } +getUser('abc' as UserId) // 正确 +getUser('abc' as PostId) // 错误! +``` + +## 类型累积(构建器模式) + +每个方法更新泛型参数: + +```typescript +interface ProcedureBuilder { + input(schema: T): ProcedureBuilder + output(schema: T): ProcedureBuilder + 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 & { + [dataTagSymbol]: TData + [errorTagSymbol]: TError +} + +// 提取标记类型 +export type InferData = T extends { [dataTagSymbol]: infer D } ? D : unknown +``` + +## 映射类型修改 + +列构建器模式(drizzle): + +```typescript +type NotNull = T & { _: { notNull: true } } +type HasDefault = T & { _: { hasDefault: true } } + +class ColumnBuilder { + notNull(): NotNull { + // ... + return this as NotNull + } + + default(value: T['data']): HasDefault { + // ... + return this as HasDefault + } +} +``` + +## 编译时错误 + +返回可读的错误信息: + +```typescript +type TypeError = { __error: Message } + +type ValidateInput = T extends string + ? T + : TypeError<'Input must be a string'> + +// 显示:类型 'TypeError<"Input must be a string">' 不可赋值给... +``` + +## 函数重载 + +针对不同输入的多重签名: + +```typescript +export function useEventListener( + event: E, + listener: (ev: WindowEventMap[E]) => any +): void + +export function useEventListener( + target: Document, + event: E, + listener: (ev: DocumentEventMap[E]) => any +): void + +export function useEventListener(...args: any[]) { + // 实现 +} +``` + +## 分发条件类型 + +应用于联合类型的每个成员: + +```typescript +type ToArray = T extends any ? T[] : never + +type Result = ToArray +// Result = string[] | number[] +``` + +使用元组禁用分发: + +```typescript +type ToArrayNonDist = [T] extends [any] ? T[] : never + +type Result = ToArrayNonDist +// Result = (string | number)[] +``` diff --git a/references/typescript-config.md b/references/typescript-config.md new file mode 100644 index 0000000..9da1d47 --- /dev/null +++ b/references/typescript-config.md @@ -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`。 diff --git a/setup/exports.md b/setup/exports.md new file mode 100644 index 0000000..b42f041 --- /dev/null +++ b/setup/exports.md @@ -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 以实现持续验证。 diff --git a/setup/project.md b/setup/project.md new file mode 100644 index 0000000..84d27f7 --- /dev/null +++ b/setup/project.md @@ -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" + } +} +``` diff --git a/setup/tsconfig.md b/setup/tsconfig.md new file mode 100644 index 0000000..a0abcc4 --- /dev/null +++ b/setup/tsconfig.md @@ -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`。 diff --git a/workflows/ci.md b/workflows/ci.md new file mode 100644 index 0000000..c668ce3 --- /dev/null +++ b/workflows/ci.md @@ -0,0 +1,9 @@ +- 逐句精准翻译,未改变语意或增删信息 +- 保留完整 Markdown 结构与 YAML frontmatter 结构不变 +- 键名保持英文原样,仅翻译英文散文值 +- 代码块、命令、路径、URL、环境变量等原样保留 +- 英文专有名词保留原文 +- 中文标点(句号、逗号),代码/URL 内标点不动 +- 无多余解释或包裹层 + +有任何需要调整的地方请告诉我。 diff --git a/workflows/release.md b/workflows/release.md new file mode 100644 index 0000000..a4ffae1 --- /dev/null +++ b/workflows/release.md @@ -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 是必需的。 diff --git a/workflows/testing.md b/workflows/testing.md new file mode 100644 index 0000000..c213a23 --- /dev/null +++ b/workflows/testing.md @@ -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>().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'], + }, +}) +```