feat(deploy): production deployment setup and fixes

- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
yeuimu
2026-08-26 14:23:09 +08:00
parent be0b90e68f
commit 6c61a4e871
982 changed files with 74156 additions and 179393 deletions
+105 -105
View File
@@ -1,105 +1,105 @@
---
name: ts-library
description: Use when authoring TypeScript libraries or npm packages - covers project setup, package.json exports, build tooling (tsdown/unbuild), API design patterns, type inference tricks, testing, and publishing to npm. Use when bundling, configuring dual CJS/ESM output, or setting up release workflows.
license: MIT
---
# TypeScript Library Development
Patterns for authoring high-quality TypeScript libraries, extracted from studying unocss, shiki, unplugin, vite, vitest, vueuse, zod, trpc, drizzle-orm, and more.
## When to Use
- Starting a new TypeScript library (single or monorepo)
- Setting up package.json exports for dual CJS/ESM
- Configuring tsconfig for library development
- Choosing build tools (tsdown, unbuild)
- Designing type-safe APIs (builder, factory, plugin patterns)
- Writing advanced TypeScript types
- Setting up vitest for library testing
- Configuring release workflow and CI
**For Nuxt module development:** use `nuxt-modules` skill
## Quick Reference
| Working on... | Load file |
| --------------------- | ------------------------------------------------------------------ |
| New project setup | [references/project-setup.md](references/project-setup.md) |
| Package exports | [references/package-exports.md](references/package-exports.md) |
| tsconfig options | [references/typescript-config.md](references/typescript-config.md) |
| Build configuration | [references/build-tooling.md](references/build-tooling.md) |
| ESLint config | [references/eslint-config.md](references/eslint-config.md) |
| API design patterns | [references/api-design.md](references/api-design.md) |
| Type inference tricks | [references/type-patterns.md](references/type-patterns.md) |
| Testing setup | [references/testing.md](references/testing.md) |
| Release workflow | [references/release.md](references/release.md) |
| CI/CD setup | [references/ci-workflows.md](references/ci-workflows.md) |
## Loading Files
**Consider loading these reference files based on your task:**
- [ ] [references/project-setup.md](references/project-setup.md) - if starting a new TypeScript library project
- [ ] [references/package-exports.md](references/package-exports.md) - if configuring package.json exports or dual CJS/ESM
- [ ] [references/typescript-config.md](references/typescript-config.md) - if setting up or modifying tsconfig.json
- [ ] [references/build-tooling.md](references/build-tooling.md) - if configuring tsdown, unbuild, or build scripts
- [ ] [references/eslint-config.md](references/eslint-config.md) - if setting up ESLint for library development
- [ ] [references/api-design.md](references/api-design.md) - if designing public APIs, builder patterns, or plugin systems
- [ ] [references/type-patterns.md](references/type-patterns.md) - if working with advanced TypeScript types or type inference
- [ ] [references/testing.md](references/testing.md) - if setting up vitest or writing tests for library code
- [ ] [references/release.md](references/release.md) - if configuring release workflow or versioning
- [ ] [references/ci-workflows.md](references/ci-workflows.md) - if setting up GitHub Actions or CI/CD pipelines
**DO NOT load all files at once.** Load only what's relevant to your current task.
## New Library Workflow
1. Create project structure → load [references/project-setup.md](references/project-setup.md)
2. Configure `package.json` exports → load [references/package-exports.md](references/package-exports.md)
3. Set up build with tsdown → load [references/build-tooling.md](references/build-tooling.md)
4. Verify build: `pnpm build && pnpm pack --dry-run` — check output includes `.mjs`, `.cjs`, `.d.ts`
5. Add tests → load [references/testing.md](references/testing.md)
6. Configure release → load [references/release.md](references/release.md)
## Quick Start
```json
// package.json (minimal)
{
"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,
})
```
## Key Principles
- ESM-first: `"type": "module"` with `.mjs` outputs
- Dual format: always support both CJS and ESM consumers
- `moduleResolution: "Bundler"` for modern TypeScript
- tsdown for most builds, unbuild for complex cases
- Smart defaults: detect environment, don't force config
- Tree-shakeable: lazy getters, proper `sideEffects: false`
_Token efficiency: Main skill ~300 tokens, each reference ~800-1200 tokens_
---
name: ts-library
description: Use when authoring TypeScript libraries or npm packages - covers project setup, package.json exports, build tooling (tsdown/unbuild), API design patterns, type inference tricks, testing, and publishing to npm. Use when bundling, configuring dual CJS/ESM output, or setting up release workflows.
license: MIT
---
# TypeScript Library Development
Patterns for authoring high-quality TypeScript libraries, extracted from studying unocss, shiki, unplugin, vite, vitest, vueuse, zod, trpc, drizzle-orm, and more.
## When to Use
- Starting a new TypeScript library (single or monorepo)
- Setting up package.json exports for dual CJS/ESM
- Configuring tsconfig for library development
- Choosing build tools (tsdown, unbuild)
- Designing type-safe APIs (builder, factory, plugin patterns)
- Writing advanced TypeScript types
- Setting up vitest for library testing
- Configuring release workflow and CI
**For Nuxt module development:** use `nuxt-modules` skill
## Quick Reference
| Working on... | Load file |
| --------------------- | ------------------------------------------------------------------ |
| New project setup | [references/project-setup.md](references/project-setup.md) |
| Package exports | [references/package-exports.md](references/package-exports.md) |
| tsconfig options | [references/typescript-config.md](references/typescript-config.md) |
| Build configuration | [references/build-tooling.md](references/build-tooling.md) |
| ESLint config | [references/eslint-config.md](references/eslint-config.md) |
| API design patterns | [references/api-design.md](references/api-design.md) |
| Type inference tricks | [references/type-patterns.md](references/type-patterns.md) |
| Testing setup | [references/testing.md](references/testing.md) |
| Release workflow | [references/release.md](references/release.md) |
| CI/CD setup | [references/ci-workflows.md](references/ci-workflows.md) |
## Loading Files
**Consider loading these reference files based on your task:**
- [ ] [references/project-setup.md](references/project-setup.md) - if starting a new TypeScript library project
- [ ] [references/package-exports.md](references/package-exports.md) - if configuring package.json exports or dual CJS/ESM
- [ ] [references/typescript-config.md](references/typescript-config.md) - if setting up or modifying tsconfig.json
- [ ] [references/build-tooling.md](references/build-tooling.md) - if configuring tsdown, unbuild, or build scripts
- [ ] [references/eslint-config.md](references/eslint-config.md) - if setting up ESLint for library development
- [ ] [references/api-design.md](references/api-design.md) - if designing public APIs, builder patterns, or plugin systems
- [ ] [references/type-patterns.md](references/type-patterns.md) - if working with advanced TypeScript types or type inference
- [ ] [references/testing.md](references/testing.md) - if setting up vitest or writing tests for library code
- [ ] [references/release.md](references/release.md) - if configuring release workflow or versioning
- [ ] [references/ci-workflows.md](references/ci-workflows.md) - if setting up GitHub Actions or CI/CD pipelines
**DO NOT load all files at once.** Load only what's relevant to your current task.
## New Library Workflow
1. Create project structure → load [references/project-setup.md](references/project-setup.md)
2. Configure `package.json` exports → load [references/package-exports.md](references/package-exports.md)
3. Set up build with tsdown → load [references/build-tooling.md](references/build-tooling.md)
4. Verify build: `pnpm build && pnpm pack --dry-run` — check output includes `.mjs`, `.cjs`, `.d.ts`
5. Add tests → load [references/testing.md](references/testing.md)
6. Configure release → load [references/release.md](references/release.md)
## Quick Start
```json
// package.json (minimal)
{
"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,
})
```
## Key Principles
- ESM-first: `"type": "module"` with `.mjs` outputs
- Dual format: always support both CJS and ESM consumers
- `moduleResolution: "Bundler"` for modern TypeScript
- tsdown for most builds, unbuild for complex cases
- Smart defaults: detect environment, don't force config
- Tree-shakeable: lazy getters, proper `sideEffects: false`
_Token efficiency: Main skill ~300 tokens, each reference ~800-1200 tokens_
@@ -1,187 +1,187 @@
# Build Tooling
## Tool Selection
| Tool | Use case |
| ------------------- | -------------------------------------------- |
| **tsdown** | Most libraries - fast, simple, modern |
| **unbuild** | Complex builds, Nuxt modules, auto-externals |
| **rollup/rolldown** | Large projects needing fine control |
## tsdown (Recommended)
```bash
pnpm add -D tsdown
```
### Basic Config
```typescript
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### Multiple Entries
```typescript
export default defineConfig({
entry: ['src/index.ts', 'src/cli.ts', 'src/utils.ts'],
format: ['esm', 'cjs'],
dts: true,
external: ['vue', 'vite'],
})
```
### Plugin Pattern (unplugin-\*)
```typescript
export default defineConfig({
entry: ['src/*.ts'], // Glob all entries
format: ['esm', 'cjs'],
dts: true,
exports: true, // Auto-generate package.json exports
attw: { profile: 'esm-only' }, // Type checking profile
})
```
### Advanced Options
```typescript
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
resolve: ['@antfu/utils'], // Inline specific deps in declarations
},
external: ['vue'],
define: {
__DEV__: 'false',
},
hooks: {
'build:done': async () => {
// Post-build tasks
},
},
})
```
## unbuild
```bash
pnpm add -D unbuild
```
### Basic Config
```typescript
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
})
```
### With Externals
```typescript
export default defineBuildConfig({
entries: ['src/index', 'src/cli'],
declaration: true,
externals: ['vue', 'vite'],
rollup: {
emitCJS: true,
inlineDependencies: true,
dts: { respectExternal: true },
},
})
```
## Output Formats
### ESM Only (modern)
```typescript
export default defineConfig({
format: ['esm'],
})
```
### Dual CJS/ESM (recommended)
```typescript
export default defineConfig({
format: ['esm', 'cjs'],
})
```
### With IIFE for CDN
```typescript
export default defineConfig([
{ format: ['esm', 'cjs'], dts: true },
{ format: 'iife', globalName: 'MyLib', minify: true },
])
```
## Define Flags
Common compile-time flags:
```typescript
export default defineConfig({
define: {
__DEV__: `(process.env.NODE_ENV !== 'production')`,
__TEST__: 'false',
__BROWSER__: 'true',
__VERSION__: JSON.stringify(pkg.version),
},
})
```
## Build Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"prepublishOnly": "pnpm build"
}
}
```
## Troubleshooting
### CJS default export issues
Some bundlers need explicit default:
```typescript
export default defineConfig({
hooks: {
'build:done': async () => {
// Patch CJS files if needed
},
},
})
```
### Missing types in output
Ensure `dts: true` and check `isolatedDeclarations` in tsconfig.
### External not working
Check package is in `peerDependencies` and listed in `external`.
# Build Tooling
## Tool Selection
| Tool | Use case |
| ------------------- | -------------------------------------------- |
| **tsdown** | Most libraries - fast, simple, modern |
| **unbuild** | Complex builds, Nuxt modules, auto-externals |
| **rollup/rolldown** | Large projects needing fine control |
## tsdown (Recommended)
```bash
pnpm add -D tsdown
```
### Basic Config
```typescript
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### Multiple Entries
```typescript
export default defineConfig({
entry: ['src/index.ts', 'src/cli.ts', 'src/utils.ts'],
format: ['esm', 'cjs'],
dts: true,
external: ['vue', 'vite'],
})
```
### Plugin Pattern (unplugin-\*)
```typescript
export default defineConfig({
entry: ['src/*.ts'], // Glob all entries
format: ['esm', 'cjs'],
dts: true,
exports: true, // Auto-generate package.json exports
attw: { profile: 'esm-only' }, // Type checking profile
})
```
### Advanced Options
```typescript
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
resolve: ['@antfu/utils'], // Inline specific deps in declarations
},
external: ['vue'],
define: {
__DEV__: 'false',
},
hooks: {
'build:done': async () => {
// Post-build tasks
},
},
})
```
## unbuild
```bash
pnpm add -D unbuild
```
### Basic Config
```typescript
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
})
```
### With Externals
```typescript
export default defineBuildConfig({
entries: ['src/index', 'src/cli'],
declaration: true,
externals: ['vue', 'vite'],
rollup: {
emitCJS: true,
inlineDependencies: true,
dts: { respectExternal: true },
},
})
```
## Output Formats
### ESM Only (modern)
```typescript
export default defineConfig({
format: ['esm'],
})
```
### Dual CJS/ESM (recommended)
```typescript
export default defineConfig({
format: ['esm', 'cjs'],
})
```
### With IIFE for CDN
```typescript
export default defineConfig([
{ format: ['esm', 'cjs'], dts: true },
{ format: 'iife', globalName: 'MyLib', minify: true },
])
```
## Define Flags
Common compile-time flags:
```typescript
export default defineConfig({
define: {
__DEV__: `(process.env.NODE_ENV !== 'production')`,
__TEST__: 'false',
__BROWSER__: 'true',
__VERSION__: JSON.stringify(pkg.version),
},
})
```
## Build Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"prepublishOnly": "pnpm build"
}
}
```
## Troubleshooting
### CJS default export issues
Some bundlers need explicit default:
```typescript
export default defineConfig({
hooks: {
'build:done': async () => {
// Patch CJS files if needed
},
},
})
```
### Missing types in output
Ensure `dts: true` and check `isolatedDeclarations` in tsconfig.
### External not working
Check package is in `peerDependencies` and listed in `external`.
@@ -1,210 +1,210 @@
# API Design Patterns
## Options Pattern
User-facing options with internal resolved version:
```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(),
}
}
```
## Factory Functions
Create configured instances:
```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() { /* ... */ },
}
}
// Usage
const ctx = createContext({ verbose: true })
await ctx.scanDirs()
```
## Builder Pattern
Chainable API with type accumulation:
```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> { /* ... */ },
}
}
// Usage - types flow through chain
const procedure = createBuilder()
.context<{ user: User }>()
.input(z.object({ id: z.string() }))
.build()
```
## Plugin Pattern (unplugin)
Universal plugin from single implementation:
```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)
},
// Bundler-specific hooks
vite: {
configResolved(config) { /* Vite-specific */ },
},
webpack(compiler) {
compiler.hooks.watchRun.tap('my-plugin', () => { /* ... */ })
},
}
})
```
Export per-bundler entries:
```typescript
// src/vite.ts
import unplugin from '.'
export default unplugin.vite
// src/webpack.ts
import unplugin from '.'
export default unplugin.webpack
```
## Lazy Getters (Tree-shaking)
Defer bundler-specific code until accessed:
```typescript
export function createPlugin<T>(factory: PluginFactory<T>) {
return {
get vite() { return getVitePlugin(factory) },
get webpack() { return getWebpackPlugin(factory) },
get rollup() { return getRollupPlugin(factory) },
}
}
```
Only the accessed getter runs, rest is tree-shaken.
## Smart Defaults
Detect environment instead of requiring config:
```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 Pattern
Flexible resolution with function or object:
```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 (Validation)
Method chaining with clone for immutability:
```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 })
}
}
// Usage
const schema = z.string().min(5).max(10).optional()
```
## Barrel Exports
Clean public API:
```typescript
// src/index.ts
export * from './config'
export * from './types'
export { createContext } from './context'
export { default } from './plugin'
```
# API Design Patterns
## Options Pattern
User-facing options with internal resolved version:
```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(),
}
}
```
## Factory Functions
Create configured instances:
```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() { /* ... */ },
}
}
// Usage
const ctx = createContext({ verbose: true })
await ctx.scanDirs()
```
## Builder Pattern
Chainable API with type accumulation:
```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> { /* ... */ },
}
}
// Usage - types flow through chain
const procedure = createBuilder()
.context<{ user: User }>()
.input(z.object({ id: z.string() }))
.build()
```
## Plugin Pattern (unplugin)
Universal plugin from single implementation:
```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)
},
// Bundler-specific hooks
vite: {
configResolved(config) { /* Vite-specific */ },
},
webpack(compiler) {
compiler.hooks.watchRun.tap('my-plugin', () => { /* ... */ })
},
}
})
```
Export per-bundler entries:
```typescript
// src/vite.ts
import unplugin from '.'
export default unplugin.vite
// src/webpack.ts
import unplugin from '.'
export default unplugin.webpack
```
## Lazy Getters (Tree-shaking)
Defer bundler-specific code until accessed:
```typescript
export function createPlugin<T>(factory: PluginFactory<T>) {
return {
get vite() { return getVitePlugin(factory) },
get webpack() { return getWebpackPlugin(factory) },
get rollup() { return getRollupPlugin(factory) },
}
}
```
Only the accessed getter runs, rest is tree-shaken.
## Smart Defaults
Detect environment instead of requiring config:
```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 Pattern
Flexible resolution with function or object:
```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 (Validation)
Method chaining with clone for immutability:
```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 })
}
}
// Usage
const schema = z.string().min(5).max(10).optional()
```
## Barrel Exports
Clean public API:
```typescript
// src/index.ts
export * from './config'
export * from './types'
export { createContext } from './context'
export { default } from './plugin'
```
@@ -1,191 +1,191 @@
# Type Patterns
## Utility Types
Common helpers used across libraries:
```typescript
// Promise or sync
export type Awaitable<T> = T | Promise<T>
// Single or array
export type Arrayable<T> = T | T[]
// Nullable
export type Nullable<T> = T | null | undefined
// Deep partial
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// Simplify intersection for better IDE display
export type Simplify<T> = { [K in keyof T]: T[K] } & {}
// Prevent inference in specific position
export type NoInfer<T> = [T][T extends any ? 0 : never]
```
## Conditional Extraction
Extract types from structures:
```typescript
// Extract input type from schema
export type Input<T> = T extends { _input: infer U } ? U : unknown
// Extract output type
export type Output<T> = T extends { _output: infer U } ? U : unknown
// Extract from nested property
export type InferContext<T> = T extends { context: infer C } ? C : never
```
## Brand Types
Nominal typing for primitives:
```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'>
// Can't mix them up
function getUser(id: UserId) { /* ... */ }
getUser('abc' as UserId) // OK
getUser('abc' as PostId) // Error!
```
## Type Accumulation (Builders)
Each method updates generic parameters:
```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
}
// Types flow through the chain
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 }))
```
## Module Augmentation
Allow users to extend library types:
```typescript
// Library code
export interface Register {}
export type DefaultError = Register extends { defaultError: infer E }
? E
: Error
// User code
declare module 'my-lib' {
interface Register {
defaultError: MyCustomError
}
}
```
## Data Tagging
Attach type metadata with symbols:
```typescript
declare const dataTagSymbol: unique symbol
declare const errorTagSymbol: unique symbol
export type DataTag<TType, TData, TError> = TType & {
[dataTagSymbol]: TData
[errorTagSymbol]: TError
}
// Extract tagged types
export type InferData<T> = T extends { [dataTagSymbol]: infer D } ? D : unknown
```
## Mapped Type Modifications
Column builder pattern (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>
}
}
```
## Compile-Time Errors
Return readable error messages:
```typescript
type TypeError<Message extends string> = { __error: Message }
type ValidateInput<T> = T extends string
? T
: TypeError<'Input must be a string'>
// Shows: Type 'TypeError<"Input must be a string">' is not assignable...
```
## Function Overloads
Multiple signatures for different inputs:
```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[]) {
// Implementation
}
```
## Distributive Conditionals
Apply to each union member:
```typescript
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// Result = string[] | number[]
```
Disable distribution with tuple:
```typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result = ToArrayNonDist<string | number>
// Result = (string | number)[]
```
# Type Patterns
## Utility Types
Common helpers used across libraries:
```typescript
// Promise or sync
export type Awaitable<T> = T | Promise<T>
// Single or array
export type Arrayable<T> = T | T[]
// Nullable
export type Nullable<T> = T | null | undefined
// Deep partial
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// Simplify intersection for better IDE display
export type Simplify<T> = { [K in keyof T]: T[K] } & {}
// Prevent inference in specific position
export type NoInfer<T> = [T][T extends any ? 0 : never]
```
## Conditional Extraction
Extract types from structures:
```typescript
// Extract input type from schema
export type Input<T> = T extends { _input: infer U } ? U : unknown
// Extract output type
export type Output<T> = T extends { _output: infer U } ? U : unknown
// Extract from nested property
export type InferContext<T> = T extends { context: infer C } ? C : never
```
## Brand Types
Nominal typing for primitives:
```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'>
// Can't mix them up
function getUser(id: UserId) { /* ... */ }
getUser('abc' as UserId) // OK
getUser('abc' as PostId) // Error!
```
## Type Accumulation (Builders)
Each method updates generic parameters:
```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
}
// Types flow through the chain
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 }))
```
## Module Augmentation
Allow users to extend library types:
```typescript
// Library code
export interface Register {}
export type DefaultError = Register extends { defaultError: infer E }
? E
: Error
// User code
declare module 'my-lib' {
interface Register {
defaultError: MyCustomError
}
}
```
## Data Tagging
Attach type metadata with symbols:
```typescript
declare const dataTagSymbol: unique symbol
declare const errorTagSymbol: unique symbol
export type DataTag<TType, TData, TError> = TType & {
[dataTagSymbol]: TData
[errorTagSymbol]: TError
}
// Extract tagged types
export type InferData<T> = T extends { [dataTagSymbol]: infer D } ? D : unknown
```
## Mapped Type Modifications
Column builder pattern (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>
}
}
```
## Compile-Time Errors
Return readable error messages:
```typescript
type TypeError<Message extends string> = { __error: Message }
type ValidateInput<T> = T extends string
? T
: TypeError<'Input must be a string'>
// Shows: Type 'TypeError<"Input must be a string">' is not assignable...
```
## Function Overloads
Multiple signatures for different inputs:
```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[]) {
// Implementation
}
```
## Distributive Conditionals
Apply to each union member:
```typescript
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// Result = string[] | number[]
```
Disable distribution with tuple:
```typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result = ToArrayNonDist<string | number>
// Result = (string | number)[]
```
@@ -1,210 +1,210 @@
# API Design Patterns
## Options Pattern
User-facing options with internal resolved version:
```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(),
}
}
```
## Factory Functions
Create configured instances:
```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() { /* ... */ },
}
}
// Usage
const ctx = createContext({ verbose: true })
await ctx.scanDirs()
```
## Builder Pattern
Chainable API with type accumulation:
```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> { /* ... */ },
}
}
// Usage - types flow through chain
const procedure = createBuilder()
.context<{ user: User }>()
.input(z.object({ id: z.string() }))
.build()
```
## Plugin Pattern (unplugin)
Universal plugin from single implementation:
```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)
},
// Bundler-specific hooks
vite: {
configResolved(config) { /* Vite-specific */ },
},
webpack(compiler) {
compiler.hooks.watchRun.tap('my-plugin', () => { /* ... */ })
},
}
})
```
Export per-bundler entries:
```typescript
// src/vite.ts
import unplugin from '.'
export default unplugin.vite
// src/webpack.ts
import unplugin from '.'
export default unplugin.webpack
```
## Lazy Getters (Tree-shaking)
Defer bundler-specific code until accessed:
```typescript
export function createPlugin<T>(factory: PluginFactory<T>) {
return {
get vite() { return getVitePlugin(factory) },
get webpack() { return getWebpackPlugin(factory) },
get rollup() { return getRollupPlugin(factory) },
}
}
```
Only the accessed getter runs, rest is tree-shaken.
## Smart Defaults
Detect environment instead of requiring config:
```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 Pattern
Flexible resolution with function or object:
```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 (Validation)
Method chaining with clone for immutability:
```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 })
}
}
// Usage
const schema = z.string().min(5).max(10).optional()
```
## Barrel Exports
Clean public API:
```typescript
// src/index.ts
export * from './config'
export * from './types'
export { createContext } from './context'
export { default } from './plugin'
```
# API Design Patterns
## Options Pattern
User-facing options with internal resolved version:
```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(),
}
}
```
## Factory Functions
Create configured instances:
```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() { /* ... */ },
}
}
// Usage
const ctx = createContext({ verbose: true })
await ctx.scanDirs()
```
## Builder Pattern
Chainable API with type accumulation:
```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> { /* ... */ },
}
}
// Usage - types flow through chain
const procedure = createBuilder()
.context<{ user: User }>()
.input(z.object({ id: z.string() }))
.build()
```
## Plugin Pattern (unplugin)
Universal plugin from single implementation:
```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)
},
// Bundler-specific hooks
vite: {
configResolved(config) { /* Vite-specific */ },
},
webpack(compiler) {
compiler.hooks.watchRun.tap('my-plugin', () => { /* ... */ })
},
}
})
```
Export per-bundler entries:
```typescript
// src/vite.ts
import unplugin from '.'
export default unplugin.vite
// src/webpack.ts
import unplugin from '.'
export default unplugin.webpack
```
## Lazy Getters (Tree-shaking)
Defer bundler-specific code until accessed:
```typescript
export function createPlugin<T>(factory: PluginFactory<T>) {
return {
get vite() { return getVitePlugin(factory) },
get webpack() { return getWebpackPlugin(factory) },
get rollup() { return getRollupPlugin(factory) },
}
}
```
Only the accessed getter runs, rest is tree-shaken.
## Smart Defaults
Detect environment instead of requiring config:
```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 Pattern
Flexible resolution with function or object:
```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 (Validation)
Method chaining with clone for immutability:
```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 })
}
}
// Usage
const schema = z.string().min(5).max(10).optional()
```
## Barrel Exports
Clean public API:
```typescript
// src/index.ts
export * from './config'
export * from './types'
export { createContext } from './context'
export { default } from './plugin'
```
@@ -1,187 +1,187 @@
# Build Tooling
## Tool Selection
| Tool | Use case |
| ------------------- | -------------------------------------------- |
| **tsdown** | Most libraries - fast, simple, modern |
| **unbuild** | Complex builds, Nuxt modules, auto-externals |
| **rollup/rolldown** | Large projects needing fine control |
## tsdown (Recommended)
```bash
pnpm add -D tsdown
```
### Basic Config
```typescript
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### Multiple Entries
```typescript
export default defineConfig({
entry: ['src/index.ts', 'src/cli.ts', 'src/utils.ts'],
format: ['esm', 'cjs'],
dts: true,
external: ['vue', 'vite'],
})
```
### Plugin Pattern (unplugin-\*)
```typescript
export default defineConfig({
entry: ['src/*.ts'], // Glob all entries
format: ['esm', 'cjs'],
dts: true,
exports: true, // Auto-generate package.json exports
attw: { profile: 'esm-only' }, // Type checking profile
})
```
### Advanced Options
```typescript
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
resolve: ['@antfu/utils'], // Inline specific deps in declarations
},
external: ['vue'],
define: {
__DEV__: 'false',
},
hooks: {
'build:done': async () => {
// Post-build tasks
},
},
})
```
## unbuild
```bash
pnpm add -D unbuild
```
### Basic Config
```typescript
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
})
```
### With Externals
```typescript
export default defineBuildConfig({
entries: ['src/index', 'src/cli'],
declaration: true,
externals: ['vue', 'vite'],
rollup: {
emitCJS: true,
inlineDependencies: true,
dts: { respectExternal: true },
},
})
```
## Output Formats
### ESM Only (modern)
```typescript
export default defineConfig({
format: ['esm'],
})
```
### Dual CJS/ESM (recommended)
```typescript
export default defineConfig({
format: ['esm', 'cjs'],
})
```
### With IIFE for CDN
```typescript
export default defineConfig([
{ format: ['esm', 'cjs'], dts: true },
{ format: 'iife', globalName: 'MyLib', minify: true },
])
```
## Define Flags
Common compile-time flags:
```typescript
export default defineConfig({
define: {
__DEV__: `(process.env.NODE_ENV !== 'production')`,
__TEST__: 'false',
__BROWSER__: 'true',
__VERSION__: JSON.stringify(pkg.version),
},
})
```
## Build Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"prepublishOnly": "pnpm build"
}
}
```
## Troubleshooting
### CJS default export issues
Some bundlers need explicit default:
```typescript
export default defineConfig({
hooks: {
'build:done': async () => {
// Patch CJS files if needed
},
},
})
```
### Missing types in output
Ensure `dts: true` and check `isolatedDeclarations` in tsconfig.
### External not working
Check package is in `peerDependencies` and listed in `external`.
# Build Tooling
## Tool Selection
| Tool | Use case |
| ------------------- | -------------------------------------------- |
| **tsdown** | Most libraries - fast, simple, modern |
| **unbuild** | Complex builds, Nuxt modules, auto-externals |
| **rollup/rolldown** | Large projects needing fine control |
## tsdown (Recommended)
```bash
pnpm add -D tsdown
```
### Basic Config
```typescript
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### Multiple Entries
```typescript
export default defineConfig({
entry: ['src/index.ts', 'src/cli.ts', 'src/utils.ts'],
format: ['esm', 'cjs'],
dts: true,
external: ['vue', 'vite'],
})
```
### Plugin Pattern (unplugin-\*)
```typescript
export default defineConfig({
entry: ['src/*.ts'], // Glob all entries
format: ['esm', 'cjs'],
dts: true,
exports: true, // Auto-generate package.json exports
attw: { profile: 'esm-only' }, // Type checking profile
})
```
### Advanced Options
```typescript
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
resolve: ['@antfu/utils'], // Inline specific deps in declarations
},
external: ['vue'],
define: {
__DEV__: 'false',
},
hooks: {
'build:done': async () => {
// Post-build tasks
},
},
})
```
## unbuild
```bash
pnpm add -D unbuild
```
### Basic Config
```typescript
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: ['src/index'],
declaration: true,
rollup: {
emitCJS: true,
},
})
```
### With Externals
```typescript
export default defineBuildConfig({
entries: ['src/index', 'src/cli'],
declaration: true,
externals: ['vue', 'vite'],
rollup: {
emitCJS: true,
inlineDependencies: true,
dts: { respectExternal: true },
},
})
```
## Output Formats
### ESM Only (modern)
```typescript
export default defineConfig({
format: ['esm'],
})
```
### Dual CJS/ESM (recommended)
```typescript
export default defineConfig({
format: ['esm', 'cjs'],
})
```
### With IIFE for CDN
```typescript
export default defineConfig([
{ format: ['esm', 'cjs'], dts: true },
{ format: 'iife', globalName: 'MyLib', minify: true },
])
```
## Define Flags
Common compile-time flags:
```typescript
export default defineConfig({
define: {
__DEV__: `(process.env.NODE_ENV !== 'production')`,
__TEST__: 'false',
__BROWSER__: 'true',
__VERSION__: JSON.stringify(pkg.version),
},
})
```
## Build Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"prepublishOnly": "pnpm build"
}
}
```
## Troubleshooting
### CJS default export issues
Some bundlers need explicit default:
```typescript
export default defineConfig({
hooks: {
'build:done': async () => {
// Patch CJS files if needed
},
},
})
```
### Missing types in output
Ensure `dts: true` and check `isolatedDeclarations` in tsconfig.
### External not working
Check package is in `peerDependencies` and listed in `external`.
@@ -1,265 +1,265 @@
# CI Workflows
## Basic 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
```
## Matrix Testing
```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
```
## Skip Docs-Only Changes
```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'
# ... rest of job
```
## Auto-fix Commits
```yaml
- run: pnpm lint:fix
- uses: stefanzweifel/git-auto-commit-action@v5
if: github.event_name == 'push'
with:
commit_message: 'chore: lint fix'
```
## Release on Tag (Token-based)
```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 }}
```
## Release on Tag (OIDC - Recommended)
No NPM_TOKEN needed. Uses GitHub OIDC for tokenless auth with 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 # Required: 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 Setup Steps
1. Open `https://www.npmjs.com/package/<PACKAGE_NAME>/access`
2. Scroll to "Publishing access" section
3. Click "Add GitHub Actions" under Trusted Publishers
4. Fill: Owner, Repository, Workflow file (`release.yml`), Environment (empty)
5. Click "Add"
### OIDC Requirements
1. **Node.js 24+** (npm 11.5.1+ required - Node 22 has npm 10.x which fails)
2. **Permissions**: `id-token: write`
3. **Publish flag**: `--provenance`
4. **package.json**: must have `repository` field
5. **npm 2FA**: "Require 2FA or granular access token" (allows OIDC)
### Troubleshooting
| Error | Cause | Fix |
| ------------------------------------- | -------------------- | ----------------------------------------- |
| "Access token expired" E404 | npm too old | Use Node.js 24 |
| ENEEDAUTH | Missing registry-url | Add `registry-url` to setup-node |
| "repository.url is empty" E422 | Missing field | Add `repository` to package.json |
| "not configured as trusted publisher" | Config mismatch | Check owner, repo, workflow match exactly |
## Monorepo Matrix
```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
```
## Concurrency Control
Cancel outdated runs:
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
```
## pkg-pr-new for PRs
```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
```
## Package Validation in CI
```yaml
- run: pnpm build
- run: pnpm dlx publint
- run: pnpm dlx @arethetypeswrong/cli --pack .
```
# CI Workflows
## Basic 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
```
## Matrix Testing
```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
```
## Skip Docs-Only Changes
```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'
# ... rest of job
```
## Auto-fix Commits
```yaml
- run: pnpm lint:fix
- uses: stefanzweifel/git-auto-commit-action@v5
if: github.event_name == 'push'
with:
commit_message: 'chore: lint fix'
```
## Release on Tag (Token-based)
```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 }}
```
## Release on Tag (OIDC - Recommended)
No NPM_TOKEN needed. Uses GitHub OIDC for tokenless auth with 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 # Required: 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 Setup Steps
1. Open `https://www.npmjs.com/package/<PACKAGE_NAME>/access`
2. Scroll to "Publishing access" section
3. Click "Add GitHub Actions" under Trusted Publishers
4. Fill: Owner, Repository, Workflow file (`release.yml`), Environment (empty)
5. Click "Add"
### OIDC Requirements
1. **Node.js 24+** (npm 11.5.1+ required - Node 22 has npm 10.x which fails)
2. **Permissions**: `id-token: write`
3. **Publish flag**: `--provenance`
4. **package.json**: must have `repository` field
5. **npm 2FA**: "Require 2FA or granular access token" (allows OIDC)
### Troubleshooting
| Error | Cause | Fix |
| ------------------------------------- | -------------------- | ----------------------------------------- |
| "Access token expired" E404 | npm too old | Use Node.js 24 |
| ENEEDAUTH | Missing registry-url | Add `registry-url` to setup-node |
| "repository.url is empty" E422 | Missing field | Add `repository` to package.json |
| "not configured as trusted publisher" | Config mismatch | Check owner, repo, workflow match exactly |
## Monorepo Matrix
```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
```
## Concurrency Control
Cancel outdated runs:
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
```
## pkg-pr-new for PRs
```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
```
## Package Validation in CI
```yaml
- run: pnpm build
- run: pnpm dlx publint
- run: pnpm dlx @arethetypeswrong/cli --pack .
```
@@ -1,120 +1,120 @@
# @antfu/eslint-config
Flat ESLint config that handles both linting and formatting - replaces Prettier.
## Setup
```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 ." } }
```
## Configuration Options
```js
import antfu from '@antfu/eslint-config'
export default antfu({
type: 'lib', // 'lib' for libraries, 'app' for applications
ignores: ['**/fixtures', '**/dist'],
stylistic: { indent: 2, quotes: 'single' },
typescript: true, // Auto-detected
vue: true, // Auto-detected
})
```
## Framework Support
| Framework | Option | Required Package |
| --------- | -------------- | ------------------------------------------------------- |
| Vue | `vue: true` | (auto-detected) |
| 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` |
## Formatters (CSS, HTML, Markdown)
For files ESLint doesn't handle natively:
```js
export default antfu({
formatters: {
css: true, // Prettier for CSS/LESS/SCSS
html: true, // Prettier for HTML
markdown: 'prettier' // or 'dprint'
}
})
// Requires: pnpm add -D eslint-plugin-format
```
## Rule Overrides
### Global
```js
export default antfu(
{ /* config options */ },
{ rules: { 'style/semi': ['error', 'never'] } }
)
```
### Per-integration
```js
export default antfu({
vue: { overrides: { 'vue/operator-linebreak': ['error', 'before'] } },
typescript: { overrides: { 'ts/consistent-type-definitions': ['error', 'interface'] } },
})
```
## Plugin Prefix Renaming
| New Prefix | Original |
| ---------- | ---------------------- |
| `ts/*` | `@typescript-eslint/*` |
| `style/*` | `@stylistic/*` |
| `import/*` | `import-lite/*` |
| `node/*` | `n/*` |
| `test/*` | `vitest/*` |
```ts
// eslint-disable-next-line ts/consistent-type-definitions
```
## Type-Aware Rules
```js
export default antfu({
typescript: { tsconfigPath: 'tsconfig.json' },
})
```
## VS Code Settings
```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"]
}
```
# @antfu/eslint-config
Flat ESLint config that handles both linting and formatting - replaces Prettier.
## Setup
```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 ." } }
```
## Configuration Options
```js
import antfu from '@antfu/eslint-config'
export default antfu({
type: 'lib', // 'lib' for libraries, 'app' for applications
ignores: ['**/fixtures', '**/dist'],
stylistic: { indent: 2, quotes: 'single' },
typescript: true, // Auto-detected
vue: true, // Auto-detected
})
```
## Framework Support
| Framework | Option | Required Package |
| --------- | -------------- | ------------------------------------------------------- |
| Vue | `vue: true` | (auto-detected) |
| 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` |
## Formatters (CSS, HTML, Markdown)
For files ESLint doesn't handle natively:
```js
export default antfu({
formatters: {
css: true, // Prettier for CSS/LESS/SCSS
html: true, // Prettier for HTML
markdown: 'prettier' // or 'dprint'
}
})
// Requires: pnpm add -D eslint-plugin-format
```
## Rule Overrides
### Global
```js
export default antfu(
{ /* config options */ },
{ rules: { 'style/semi': ['error', 'never'] } }
)
```
### Per-integration
```js
export default antfu({
vue: { overrides: { 'vue/operator-linebreak': ['error', 'before'] } },
typescript: { overrides: { 'ts/consistent-type-definitions': ['error', 'interface'] } },
})
```
## Plugin Prefix Renaming
| New Prefix | Original |
| ---------- | ---------------------- |
| `ts/*` | `@typescript-eslint/*` |
| `style/*` | `@stylistic/*` |
| `import/*` | `import-lite/*` |
| `node/*` | `n/*` |
| `test/*` | `vitest/*` |
```ts
// eslint-disable-next-line ts/consistent-type-definitions
```
## Type-Aware Rules
```js
export default antfu({
typescript: { tsconfigPath: 'tsconfig.json' },
})
```
## VS Code Settings
```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"]
}
```
@@ -1,154 +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-\*)
```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
For older TypeScript versions without exports support:
```json
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./*"]
}
}
}
```
## Field Reference
| Field | Purpose |
| ------------- | -------------------------------- |
| `exports` | Modern entry points (Node 12.7+) |
| `main` | CJS fallback for older bundlers |
| `module` | ESM fallback for bundlers |
| `types` | TypeScript fallback |
| `sideEffects` | `false` enables tree-shaking |
| `files` | What gets published to npm |
## Condition Order
Order matters! Put most specific first:
```json
{
".": {
"types": "...", // Always first
"import": "...", // ESM
"require": "..." // CJS fallback
}
}
```
## Peer Dependencies
External deps that consumers must provide:
```json
{
"peerDependencies": {
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"vue": { "optional": true }
}
}
```
## Package Validation
```bash
# Check exports are correct
pnpm dlx publint
pnpm dlx @arethetypeswrong/cli
```
Add to CI for continuous validation.
# 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-\*)
```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
For older TypeScript versions without exports support:
```json
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./*"]
}
}
}
```
## Field Reference
| Field | Purpose |
| ------------- | -------------------------------- |
| `exports` | Modern entry points (Node 12.7+) |
| `main` | CJS fallback for older bundlers |
| `module` | ESM fallback for bundlers |
| `types` | TypeScript fallback |
| `sideEffects` | `false` enables tree-shaking |
| `files` | What gets published to npm |
## Condition Order
Order matters! Put most specific first:
```json
{
".": {
"types": "...", // Always first
"import": "...", // ESM
"require": "..." // CJS fallback
}
}
```
## Peer Dependencies
External deps that consumers must provide:
```json
{
"peerDependencies": {
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"vue": { "optional": true }
}
}
```
## Package Validation
```bash
# Check exports are correct
pnpm dlx publint
pnpm dlx @arethetypeswrong/cli
```
Add to CI for continuous validation.
@@ -1,157 +1,157 @@
# Project Setup
## Single Package
```bash
# Clone starter template
cp -r ~/templates/antfu/starter-ts my-lib
cd my-lib && rm -rf .git && git init
pnpm install
```
Or manual setup:
```bash
mkdir my-lib && cd my-lib
pnpm init
pnpm add -D typescript tsdown vitest eslint @antfu/eslint-config
```
### Directory Structure
```
my-lib/
├── src/
│ ├── index.ts # Main entry
│ └── types.ts # Type definitions
├── test/
│ └── index.test.ts
├── dist/ # Build output (gitignored)
├── package.json
├── tsconfig.json
├── tsdown.config.ts
├── eslint.config.ts
└── vitest.config.ts
```
## Monorepo
```bash
cp -r ~/templates/antfu/starter-monorepo my-monorepo
cd my-monorepo && rm -rf .git && git init
pnpm install
```
### Structure
```
my-monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsdown.config.ts
│ └── cli/
│ ├── src/
│ └── package.json
├── playground/ # Integration tests
├── pnpm-workspace.yaml
├── package.json # Root scripts, devDeps
├── tsconfig.json # Base config
└── 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
Organize dependencies by purpose (from antfu's blog post):
| Category | Contents |
| -------- | ---------------------------------- |
| build | tsdown, unbuild, rollup plugins |
| lint | eslint, @antfu/eslint-config |
| test | vitest, @vue/test-utils |
| types | typescript, @types/\* |
| prod | Runtime deps: consola, defu, pathe |
### Using Catalogs
```json
{
"devDependencies": {
"tsdown": "catalog:build",
"eslint": "catalog:lint",
"vitest": "catalog:test",
"typescript": "catalog:types"
}
}
```
## ESLint Setup
```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" }
}
```
Run `pnpm prepare` after adding.
## Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit",
"test": "vitest",
"release": "bumpp",
"prepublishOnly": "pnpm build"
}
}
```
# Project Setup
## Single Package
```bash
# Clone starter template
cp -r ~/templates/antfu/starter-ts my-lib
cd my-lib && rm -rf .git && git init
pnpm install
```
Or manual setup:
```bash
mkdir my-lib && cd my-lib
pnpm init
pnpm add -D typescript tsdown vitest eslint @antfu/eslint-config
```
### Directory Structure
```
my-lib/
├── src/
│ ├── index.ts # Main entry
│ └── types.ts # Type definitions
├── test/
│ └── index.test.ts
├── dist/ # Build output (gitignored)
├── package.json
├── tsconfig.json
├── tsdown.config.ts
├── eslint.config.ts
└── vitest.config.ts
```
## Monorepo
```bash
cp -r ~/templates/antfu/starter-monorepo my-monorepo
cd my-monorepo && rm -rf .git && git init
pnpm install
```
### Structure
```
my-monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsdown.config.ts
│ └── cli/
│ ├── src/
│ └── package.json
├── playground/ # Integration tests
├── pnpm-workspace.yaml
├── package.json # Root scripts, devDeps
├── tsconfig.json # Base config
└── 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
Organize dependencies by purpose (from antfu's blog post):
| Category | Contents |
| -------- | ---------------------------------- |
| build | tsdown, unbuild, rollup plugins |
| lint | eslint, @antfu/eslint-config |
| test | vitest, @vue/test-utils |
| types | typescript, @types/\* |
| prod | Runtime deps: consola, defu, pathe |
### Using Catalogs
```json
{
"devDependencies": {
"tsdown": "catalog:build",
"eslint": "catalog:lint",
"vitest": "catalog:test",
"typescript": "catalog:types"
}
}
```
## ESLint Setup
```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" }
}
```
Run `pnpm prepare` after adding.
## Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit",
"test": "vitest",
"release": "bumpp",
"prepublishOnly": "pnpm build"
}
}
```
@@ -1,180 +1,180 @@
# Release Workflow
## Tools
| Tool | Purpose |
| ----------- | --------------------------------- |
| bumpp | Interactive version bumping |
| changelogen | Changelog generation from commits |
| pkg-pr-new | PR preview packages |
## bumpp (Version Bumping)
```bash
pnpm add -D bumpp
```
```json
{
"scripts": {
"release": "bumpp"
}
}
```
Interactive prompt for patch/minor/major. Options:
```json
{
"scripts": {
"release": "bumpp --commit --tag --push"
}
}
```
For monorepos:
```bash
bumpp -r # Recursive
bumpp packages/*/package.json # Specific packages
```
## changelogen (Changelog)
```bash
pnpm add -D changelogen
```
```json
{
"scripts": {
"changelog": "changelogen --release"
}
}
```
Combined workflow:
```json
{
"scripts": {
"release": "changelogen --release && bumpp"
}
}
```
## Full Release Flow
```json
{
"scripts": {
"release": "pnpm lint && pnpm test && changelogen --release && bumpp --commit --tag --push"
}
}
```
CI publishes to npm on tag push.
## pkg-pr-new (PR Previews)
For publishable packages. Creates install links on PRs.
```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
```
For monorepos:
```bash
pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*'
```
PR comment shows:
```
pnpm add https://pkg.pr.new/your-org/your-package@123
```
## Conventional Commits
For changelogen to work:
```
feat: add dark mode support
fix: resolve memory leak in parser
docs: update README
chore: update dependencies
```
## npm Publishing
### Token-based (legacy)
```yaml
- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### OIDC (Recommended)
No token needed. See ci-workflows.md for full setup.
```yaml
- run: pnpm publish --access public --no-git-checks --provenance
```
## Monorepo Publishing
With pnpm:
```bash
pnpm -r publish --access public
```
With bumpp:
```bash
bumpp -r && pnpm -r publish
```
## Pre-release Versions
```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 Requirements
```json
{
"name": "@scope/package",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/org/repo.git"
},
"publishConfig": {
"access": "public"
}
}
```
`repository` required for npm provenance.
# Release Workflow
## Tools
| Tool | Purpose |
| ----------- | --------------------------------- |
| bumpp | Interactive version bumping |
| changelogen | Changelog generation from commits |
| pkg-pr-new | PR preview packages |
## bumpp (Version Bumping)
```bash
pnpm add -D bumpp
```
```json
{
"scripts": {
"release": "bumpp"
}
}
```
Interactive prompt for patch/minor/major. Options:
```json
{
"scripts": {
"release": "bumpp --commit --tag --push"
}
}
```
For monorepos:
```bash
bumpp -r # Recursive
bumpp packages/*/package.json # Specific packages
```
## changelogen (Changelog)
```bash
pnpm add -D changelogen
```
```json
{
"scripts": {
"changelog": "changelogen --release"
}
}
```
Combined workflow:
```json
{
"scripts": {
"release": "changelogen --release && bumpp"
}
}
```
## Full Release Flow
```json
{
"scripts": {
"release": "pnpm lint && pnpm test && changelogen --release && bumpp --commit --tag --push"
}
}
```
CI publishes to npm on tag push.
## pkg-pr-new (PR Previews)
For publishable packages. Creates install links on PRs.
```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
```
For monorepos:
```bash
pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*'
```
PR comment shows:
```
pnpm add https://pkg.pr.new/your-org/your-package@123
```
## Conventional Commits
For changelogen to work:
```
feat: add dark mode support
fix: resolve memory leak in parser
docs: update README
chore: update dependencies
```
## npm Publishing
### Token-based (legacy)
```yaml
- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### OIDC (Recommended)
No token needed. See ci-workflows.md for full setup.
```yaml
- run: pnpm publish --access public --no-git-checks --provenance
```
## Monorepo Publishing
With pnpm:
```bash
pnpm -r publish --access public
```
With bumpp:
```bash
bumpp -r && pnpm -r publish
```
## Pre-release Versions
```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 Requirements
```json
{
"name": "@scope/package",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/org/repo.git"
},
"publishConfig": {
"access": "public"
}
}
```
`repository` required for npm provenance.
@@ -1,201 +1,201 @@
# Testing
## Vitest Setup
```bash
pnpm add -D vitest
```
### Basic Config
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
testTimeout: 30_000,
reporters: 'dot',
},
})
```
### With Coverage
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/types.ts'],
reporter: ['text', 'lcovonly', 'html'],
},
},
})
```
## Workspace Projects
For monorepos, test packages separately:
```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-Based Testing
Test transforms with file fixtures:
```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()
})
}
})
```
## Idempotency Testing
Ensure transforms are stable:
```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) // Should not change
})
```
## Type-Level Testing
Test TypeScript types:
```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 }>()
})
})
```
## Multi-TS Version Testing
Test across TypeScript versions (TanStack pattern):
```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
```
## Package Validation
Validate published package:
```bash
# Check exports are correct
pnpm dlx publint
# Check types work in different moduleResolutions
pnpm dlx @arethetypeswrong/cli --pack .
```
Add to tsdown config:
```typescript
export default defineConfig({
attw: { profile: 'esm-only' }, // or 'node16'
})
```
## Test Scripts
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:types": "vitest typecheck"
}
}
```
## Mocking
```typescript
import { vi } from 'vitest'
vi.mock('fs', () => ({
readFileSync: vi.fn(() => 'mocked content'),
}))
// Spy on method
const spy = vi.spyOn(console, 'log')
expect(spy).toHaveBeenCalledWith('expected')
```
## Testing Plugins
Dogfood your own plugin in tests:
```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'],
},
})
```
# Testing
## Vitest Setup
```bash
pnpm add -D vitest
```
### Basic Config
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
testTimeout: 30_000,
reporters: 'dot',
},
})
```
### With Coverage
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/types.ts'],
reporter: ['text', 'lcovonly', 'html'],
},
},
})
```
## Workspace Projects
For monorepos, test packages separately:
```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-Based Testing
Test transforms with file fixtures:
```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()
})
}
})
```
## Idempotency Testing
Ensure transforms are stable:
```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) // Should not change
})
```
## Type-Level Testing
Test TypeScript types:
```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 }>()
})
})
```
## Multi-TS Version Testing
Test across TypeScript versions (TanStack pattern):
```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
```
## Package Validation
Validate published package:
```bash
# Check exports are correct
pnpm dlx publint
# Check types work in different moduleResolutions
pnpm dlx @arethetypeswrong/cli --pack .
```
Add to tsdown config:
```typescript
export default defineConfig({
attw: { profile: 'esm-only' }, // or 'node16'
})
```
## Test Scripts
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:types": "vitest typecheck"
}
}
```
## Mocking
```typescript
import { vi } from 'vitest'
vi.mock('fs', () => ({
readFileSync: vi.fn(() => 'mocked content'),
}))
// Spy on method
const spy = vi.spyOn(console, 'log')
expect(spy).toHaveBeenCalledWith('expected')
```
## Testing Plugins
Dogfood your own plugin in tests:
```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'],
},
})
```
@@ -1,191 +1,191 @@
# Type Patterns
## Utility Types
Common helpers used across libraries:
```typescript
// Promise or sync
export type Awaitable<T> = T | Promise<T>
// Single or array
export type Arrayable<T> = T | T[]
// Nullable
export type Nullable<T> = T | null | undefined
// Deep partial
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// Simplify intersection for better IDE display
export type Simplify<T> = { [K in keyof T]: T[K] } & {}
// Prevent inference in specific position
export type NoInfer<T> = [T][T extends any ? 0 : never]
```
## Conditional Extraction
Extract types from structures:
```typescript
// Extract input type from schema
export type Input<T> = T extends { _input: infer U } ? U : unknown
// Extract output type
export type Output<T> = T extends { _output: infer U } ? U : unknown
// Extract from nested property
export type InferContext<T> = T extends { context: infer C } ? C : never
```
## Brand Types
Nominal typing for primitives:
```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'>
// Can't mix them up
function getUser(id: UserId) { /* ... */ }
getUser('abc' as UserId) // OK
getUser('abc' as PostId) // Error!
```
## Type Accumulation (Builders)
Each method updates generic parameters:
```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
}
// Types flow through the chain
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 }))
```
## Module Augmentation
Allow users to extend library types:
```typescript
// Library code
export interface Register {}
export type DefaultError = Register extends { defaultError: infer E }
? E
: Error
// User code
declare module 'my-lib' {
interface Register {
defaultError: MyCustomError
}
}
```
## Data Tagging
Attach type metadata with symbols:
```typescript
declare const dataTagSymbol: unique symbol
declare const errorTagSymbol: unique symbol
export type DataTag<TType, TData, TError> = TType & {
[dataTagSymbol]: TData
[errorTagSymbol]: TError
}
// Extract tagged types
export type InferData<T> = T extends { [dataTagSymbol]: infer D } ? D : unknown
```
## Mapped Type Modifications
Column builder pattern (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>
}
}
```
## Compile-Time Errors
Return readable error messages:
```typescript
type TypeError<Message extends string> = { __error: Message }
type ValidateInput<T> = T extends string
? T
: TypeError<'Input must be a string'>
// Shows: Type 'TypeError<"Input must be a string">' is not assignable...
```
## Function Overloads
Multiple signatures for different inputs:
```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[]) {
// Implementation
}
```
## Distributive Conditionals
Apply to each union member:
```typescript
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// Result = string[] | number[]
```
Disable distribution with tuple:
```typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result = ToArrayNonDist<string | number>
// Result = (string | number)[]
```
# Type Patterns
## Utility Types
Common helpers used across libraries:
```typescript
// Promise or sync
export type Awaitable<T> = T | Promise<T>
// Single or array
export type Arrayable<T> = T | T[]
// Nullable
export type Nullable<T> = T | null | undefined
// Deep partial
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// Simplify intersection for better IDE display
export type Simplify<T> = { [K in keyof T]: T[K] } & {}
// Prevent inference in specific position
export type NoInfer<T> = [T][T extends any ? 0 : never]
```
## Conditional Extraction
Extract types from structures:
```typescript
// Extract input type from schema
export type Input<T> = T extends { _input: infer U } ? U : unknown
// Extract output type
export type Output<T> = T extends { _output: infer U } ? U : unknown
// Extract from nested property
export type InferContext<T> = T extends { context: infer C } ? C : never
```
## Brand Types
Nominal typing for primitives:
```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'>
// Can't mix them up
function getUser(id: UserId) { /* ... */ }
getUser('abc' as UserId) // OK
getUser('abc' as PostId) // Error!
```
## Type Accumulation (Builders)
Each method updates generic parameters:
```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
}
// Types flow through the chain
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 }))
```
## Module Augmentation
Allow users to extend library types:
```typescript
// Library code
export interface Register {}
export type DefaultError = Register extends { defaultError: infer E }
? E
: Error
// User code
declare module 'my-lib' {
interface Register {
defaultError: MyCustomError
}
}
```
## Data Tagging
Attach type metadata with symbols:
```typescript
declare const dataTagSymbol: unique symbol
declare const errorTagSymbol: unique symbol
export type DataTag<TType, TData, TError> = TType & {
[dataTagSymbol]: TData
[errorTagSymbol]: TError
}
// Extract tagged types
export type InferData<T> = T extends { [dataTagSymbol]: infer D } ? D : unknown
```
## Mapped Type Modifications
Column builder pattern (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>
}
}
```
## Compile-Time Errors
Return readable error messages:
```typescript
type TypeError<Message extends string> = { __error: Message }
type ValidateInput<T> = T extends string
? T
: TypeError<'Input must be a string'>
// Shows: Type 'TypeError<"Input must be a string">' is not assignable...
```
## Function Overloads
Multiple signatures for different inputs:
```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[]) {
// Implementation
}
```
## Distributive Conditionals
Apply to each union member:
```typescript
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// Result = string[] | number[]
```
Disable distribution with tuple:
```typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result = ToArrayNonDist<string | number>
// Result = (string | number)[]
```
@@ -1,144 +1,144 @@
# TypeScript Configuration
## Library Base Config
```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"]
}
```
## Key Options Explained
| Option | Value | Why |
| ---------------------- | ------- | ------------------------------------------------ |
| `target` | ESNext | Modern output, bundlers downgrade |
| `module` | ESNext | ESM output |
| `moduleResolution` | Bundler | Works with modern bundlers, allows no extensions |
| `strict` | true | Catch errors early |
| `noEmit` | true | Build tool handles emit |
| `isolatedDeclarations` | true | Faster DTS generation |
| `verbatimModuleSyntax` | true | Explicit `import type` required |
| `skipLibCheck` | true | Faster builds |
## Monorepo Config
### Root tsconfig.json
```json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true
}
}
```
### Package tsconfig.json
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [
{ "path": "../utils" }
]
}
```
## Path Aliases
For internal imports in monorepos:
```json
{
"compilerOptions": {
"paths": {
"@my-lib/core": ["./packages/core/src"],
"@my-lib/utils": ["./packages/utils/src"],
"#internal/*": ["./virtual-shared/*"]
}
}
}
```
## Bundler vs Node Resolution
**Use `Bundler`** for libraries consumed by bundlers (Vite, webpack, etc.):
- Allows importing without extensions
- Supports `exports` field in package.json
- Modern, simpler setup
**Use `Node16/NodeNext`** for Node.js-only libraries:
- Requires explicit extensions (`.js`)
- Stricter, matches Node.js behavior exactly
## Type Declarations
Let build tool generate declarations:
```typescript
// tsdown.config.ts
export default defineConfig({
dts: true, // Generate .d.ts
dts: { resolve: ['@antfu/utils'] } // Inline specific types
})
```
Or with unbuild:
```typescript
// build.config.ts
export default defineBuildConfig({
declaration: 'node16', // For Node.js compatibility
declaration: true, // For bundler resolution
})
```
## Common Issues
### Module not found errors
Check `moduleResolution` matches your target:
- Bundler: `"Bundler"`
- Node.js: `"Node16"` or `"NodeNext"`
### Type imports not working
Enable `verbatimModuleSyntax` and use explicit:
```typescript
import type { Foo } from './types'
```
### Slow type checking
Enable `skipLibCheck: true` and `isolatedDeclarations: true`.
# TypeScript Configuration
## Library Base Config
```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"]
}
```
## Key Options Explained
| Option | Value | Why |
| ---------------------- | ------- | ------------------------------------------------ |
| `target` | ESNext | Modern output, bundlers downgrade |
| `module` | ESNext | ESM output |
| `moduleResolution` | Bundler | Works with modern bundlers, allows no extensions |
| `strict` | true | Catch errors early |
| `noEmit` | true | Build tool handles emit |
| `isolatedDeclarations` | true | Faster DTS generation |
| `verbatimModuleSyntax` | true | Explicit `import type` required |
| `skipLibCheck` | true | Faster builds |
## Monorepo Config
### Root tsconfig.json
```json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true
}
}
```
### Package tsconfig.json
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [
{ "path": "../utils" }
]
}
```
## Path Aliases
For internal imports in monorepos:
```json
{
"compilerOptions": {
"paths": {
"@my-lib/core": ["./packages/core/src"],
"@my-lib/utils": ["./packages/utils/src"],
"#internal/*": ["./virtual-shared/*"]
}
}
}
```
## Bundler vs Node Resolution
**Use `Bundler`** for libraries consumed by bundlers (Vite, webpack, etc.):
- Allows importing without extensions
- Supports `exports` field in package.json
- Modern, simpler setup
**Use `Node16/NodeNext`** for Node.js-only libraries:
- Requires explicit extensions (`.js`)
- Stricter, matches Node.js behavior exactly
## Type Declarations
Let build tool generate declarations:
```typescript
// tsdown.config.ts
export default defineConfig({
dts: true, // Generate .d.ts
dts: { resolve: ['@antfu/utils'] } // Inline specific types
})
```
Or with unbuild:
```typescript
// build.config.ts
export default defineBuildConfig({
declaration: 'node16', // For Node.js compatibility
declaration: true, // For bundler resolution
})
```
## Common Issues
### Module not found errors
Check `moduleResolution` matches your target:
- Bundler: `"Bundler"`
- Node.js: `"Node16"` or `"NodeNext"`
### Type imports not working
Enable `verbatimModuleSyntax` and use explicit:
```typescript
import type { Foo } from './types'
```
### Slow type checking
Enable `skipLibCheck: true` and `isolatedDeclarations: true`.
@@ -1,154 +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-\*)
```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
For older TypeScript versions without exports support:
```json
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./*"]
}
}
}
```
## Field Reference
| Field | Purpose |
| ------------- | -------------------------------- |
| `exports` | Modern entry points (Node 12.7+) |
| `main` | CJS fallback for older bundlers |
| `module` | ESM fallback for bundlers |
| `types` | TypeScript fallback |
| `sideEffects` | `false` enables tree-shaking |
| `files` | What gets published to npm |
## Condition Order
Order matters! Put most specific first:
```json
{
".": {
"types": "...", // Always first
"import": "...", // ESM
"require": "..." // CJS fallback
}
}
```
## Peer Dependencies
External deps that consumers must provide:
```json
{
"peerDependencies": {
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"vue": { "optional": true }
}
}
```
## Package Validation
```bash
# Check exports are correct
pnpm dlx publint
pnpm dlx @arethetypeswrong/cli
```
Add to CI for continuous validation.
# 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-\*)
```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
For older TypeScript versions without exports support:
```json
{
"typesVersions": {
"*": {
"*": ["./dist/*", "./*"]
}
}
}
```
## Field Reference
| Field | Purpose |
| ------------- | -------------------------------- |
| `exports` | Modern entry points (Node 12.7+) |
| `main` | CJS fallback for older bundlers |
| `module` | ESM fallback for bundlers |
| `types` | TypeScript fallback |
| `sideEffects` | `false` enables tree-shaking |
| `files` | What gets published to npm |
## Condition Order
Order matters! Put most specific first:
```json
{
".": {
"types": "...", // Always first
"import": "...", // ESM
"require": "..." // CJS fallback
}
}
```
## Peer Dependencies
External deps that consumers must provide:
```json
{
"peerDependencies": {
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"vue": { "optional": true }
}
}
```
## Package Validation
```bash
# Check exports are correct
pnpm dlx publint
pnpm dlx @arethetypeswrong/cli
```
Add to CI for continuous validation.
@@ -1,157 +1,157 @@
# Project Setup
## Single Package
```bash
# Clone starter template
cp -r ~/templates/antfu/starter-ts my-lib
cd my-lib && rm -rf .git && git init
pnpm install
```
Or manual setup:
```bash
mkdir my-lib && cd my-lib
pnpm init
pnpm add -D typescript tsdown vitest eslint @antfu/eslint-config
```
### Directory Structure
```
my-lib/
├── src/
│ ├── index.ts # Main entry
│ └── types.ts # Type definitions
├── test/
│ └── index.test.ts
├── dist/ # Build output (gitignored)
├── package.json
├── tsconfig.json
├── tsdown.config.ts
├── eslint.config.ts
└── vitest.config.ts
```
## Monorepo
```bash
cp -r ~/templates/antfu/starter-monorepo my-monorepo
cd my-monorepo && rm -rf .git && git init
pnpm install
```
### Structure
```
my-monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsdown.config.ts
│ └── cli/
│ ├── src/
│ └── package.json
├── playground/ # Integration tests
├── pnpm-workspace.yaml
├── package.json # Root scripts, devDeps
├── tsconfig.json # Base config
└── 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
Organize dependencies by purpose (from antfu's blog post):
| Category | Contents |
| -------- | ---------------------------------- |
| build | tsdown, unbuild, rollup plugins |
| lint | eslint, @antfu/eslint-config |
| test | vitest, @vue/test-utils |
| types | typescript, @types/\* |
| prod | Runtime deps: consola, defu, pathe |
### Using Catalogs
```json
{
"devDependencies": {
"tsdown": "catalog:build",
"eslint": "catalog:lint",
"vitest": "catalog:test",
"typescript": "catalog:types"
}
}
```
## ESLint Setup
```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" }
}
```
Run `pnpm prepare` after adding.
## Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit",
"test": "vitest",
"release": "bumpp",
"prepublishOnly": "pnpm build"
}
}
```
# Project Setup
## Single Package
```bash
# Clone starter template
cp -r ~/templates/antfu/starter-ts my-lib
cd my-lib && rm -rf .git && git init
pnpm install
```
Or manual setup:
```bash
mkdir my-lib && cd my-lib
pnpm init
pnpm add -D typescript tsdown vitest eslint @antfu/eslint-config
```
### Directory Structure
```
my-lib/
├── src/
│ ├── index.ts # Main entry
│ └── types.ts # Type definitions
├── test/
│ └── index.test.ts
├── dist/ # Build output (gitignored)
├── package.json
├── tsconfig.json
├── tsdown.config.ts
├── eslint.config.ts
└── vitest.config.ts
```
## Monorepo
```bash
cp -r ~/templates/antfu/starter-monorepo my-monorepo
cd my-monorepo && rm -rf .git && git init
pnpm install
```
### Structure
```
my-monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsdown.config.ts
│ └── cli/
│ ├── src/
│ └── package.json
├── playground/ # Integration tests
├── pnpm-workspace.yaml
├── package.json # Root scripts, devDeps
├── tsconfig.json # Base config
└── 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
Organize dependencies by purpose (from antfu's blog post):
| Category | Contents |
| -------- | ---------------------------------- |
| build | tsdown, unbuild, rollup plugins |
| lint | eslint, @antfu/eslint-config |
| test | vitest, @vue/test-utils |
| types | typescript, @types/\* |
| prod | Runtime deps: consola, defu, pathe |
### Using Catalogs
```json
{
"devDependencies": {
"tsdown": "catalog:build",
"eslint": "catalog:lint",
"vitest": "catalog:test",
"typescript": "catalog:types"
}
}
```
## ESLint Setup
```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" }
}
```
Run `pnpm prepare` after adding.
## Scripts
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit",
"test": "vitest",
"release": "bumpp",
"prepublishOnly": "pnpm build"
}
}
```
@@ -1,144 +1,144 @@
# TypeScript Configuration
## Library Base Config
```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"]
}
```
## Key Options Explained
| Option | Value | Why |
| ---------------------- | ------- | ------------------------------------------------ |
| `target` | ESNext | Modern output, bundlers downgrade |
| `module` | ESNext | ESM output |
| `moduleResolution` | Bundler | Works with modern bundlers, allows no extensions |
| `strict` | true | Catch errors early |
| `noEmit` | true | Build tool handles emit |
| `isolatedDeclarations` | true | Faster DTS generation |
| `verbatimModuleSyntax` | true | Explicit `import type` required |
| `skipLibCheck` | true | Faster builds |
## Monorepo Config
### Root tsconfig.json
```json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true
}
}
```
### Package tsconfig.json
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [
{ "path": "../utils" }
]
}
```
## Path Aliases
For internal imports in monorepos:
```json
{
"compilerOptions": {
"paths": {
"@my-lib/core": ["./packages/core/src"],
"@my-lib/utils": ["./packages/utils/src"],
"#internal/*": ["./virtual-shared/*"]
}
}
}
```
## Bundler vs Node Resolution
**Use `Bundler`** for libraries consumed by bundlers (Vite, webpack, etc.):
- Allows importing without extensions
- Supports `exports` field in package.json
- Modern, simpler setup
**Use `Node16/NodeNext`** for Node.js-only libraries:
- Requires explicit extensions (`.js`)
- Stricter, matches Node.js behavior exactly
## Type Declarations
Let build tool generate declarations:
```typescript
// tsdown.config.ts
export default defineConfig({
dts: true, // Generate .d.ts
dts: { resolve: ['@antfu/utils'] } // Inline specific types
})
```
Or with unbuild:
```typescript
// build.config.ts
export default defineBuildConfig({
declaration: 'node16', // For Node.js compatibility
declaration: true, // For bundler resolution
})
```
## Common Issues
### Module not found errors
Check `moduleResolution` matches your target:
- Bundler: `"Bundler"`
- Node.js: `"Node16"` or `"NodeNext"`
### Type imports not working
Enable `verbatimModuleSyntax` and use explicit:
```typescript
import type { Foo } from './types'
```
### Slow type checking
Enable `skipLibCheck: true` and `isolatedDeclarations: true`.
# TypeScript Configuration
## Library Base Config
```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"]
}
```
## Key Options Explained
| Option | Value | Why |
| ---------------------- | ------- | ------------------------------------------------ |
| `target` | ESNext | Modern output, bundlers downgrade |
| `module` | ESNext | ESM output |
| `moduleResolution` | Bundler | Works with modern bundlers, allows no extensions |
| `strict` | true | Catch errors early |
| `noEmit` | true | Build tool handles emit |
| `isolatedDeclarations` | true | Faster DTS generation |
| `verbatimModuleSyntax` | true | Explicit `import type` required |
| `skipLibCheck` | true | Faster builds |
## Monorepo Config
### Root tsconfig.json
```json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true
}
}
```
### Package tsconfig.json
```json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [
{ "path": "../utils" }
]
}
```
## Path Aliases
For internal imports in monorepos:
```json
{
"compilerOptions": {
"paths": {
"@my-lib/core": ["./packages/core/src"],
"@my-lib/utils": ["./packages/utils/src"],
"#internal/*": ["./virtual-shared/*"]
}
}
}
```
## Bundler vs Node Resolution
**Use `Bundler`** for libraries consumed by bundlers (Vite, webpack, etc.):
- Allows importing without extensions
- Supports `exports` field in package.json
- Modern, simpler setup
**Use `Node16/NodeNext`** for Node.js-only libraries:
- Requires explicit extensions (`.js`)
- Stricter, matches Node.js behavior exactly
## Type Declarations
Let build tool generate declarations:
```typescript
// tsdown.config.ts
export default defineConfig({
dts: true, // Generate .d.ts
dts: { resolve: ['@antfu/utils'] } // Inline specific types
})
```
Or with unbuild:
```typescript
// build.config.ts
export default defineBuildConfig({
declaration: 'node16', // For Node.js compatibility
declaration: true, // For bundler resolution
})
```
## Common Issues
### Module not found errors
Check `moduleResolution` matches your target:
- Bundler: `"Bundler"`
- Node.js: `"Node16"` or `"NodeNext"`
### Type imports not working
Enable `verbatimModuleSyntax` and use explicit:
```typescript
import type { Foo } from './types'
```
### Slow type checking
Enable `skipLibCheck: true` and `isolatedDeclarations: true`.
@@ -1,265 +1,265 @@
# CI Workflows
## Basic 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
```
## Matrix Testing
```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
```
## Skip Docs-Only Changes
```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'
# ... rest of job
```
## Auto-fix Commits
```yaml
- run: pnpm lint:fix
- uses: stefanzweifel/git-auto-commit-action@v5
if: github.event_name == 'push'
with:
commit_message: 'chore: lint fix'
```
## Release on Tag (Token-based)
```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 }}
```
## Release on Tag (OIDC - Recommended)
No NPM_TOKEN needed. Uses GitHub OIDC for tokenless auth with 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 # Required: 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 Setup Steps
1. Open `https://www.npmjs.com/package/<PACKAGE_NAME>/access`
2. Scroll to "Publishing access" section
3. Click "Add GitHub Actions" under Trusted Publishers
4. Fill: Owner, Repository, Workflow file (`release.yml`), Environment (empty)
5. Click "Add"
### OIDC Requirements
1. **Node.js 24+** (npm 11.5.1+ required - Node 22 has npm 10.x which fails)
2. **Permissions**: `id-token: write`
3. **Publish flag**: `--provenance`
4. **package.json**: must have `repository` field
5. **npm 2FA**: "Require 2FA or granular access token" (allows OIDC)
### Troubleshooting
| Error | Cause | Fix |
| ------------------------------------- | -------------------- | ----------------------------------------- |
| "Access token expired" E404 | npm too old | Use Node.js 24 |
| ENEEDAUTH | Missing registry-url | Add `registry-url` to setup-node |
| "repository.url is empty" E422 | Missing field | Add `repository` to package.json |
| "not configured as trusted publisher" | Config mismatch | Check owner, repo, workflow match exactly |
## Monorepo Matrix
```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
```
## Concurrency Control
Cancel outdated runs:
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
```
## pkg-pr-new for PRs
```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
```
## Package Validation in CI
```yaml
- run: pnpm build
- run: pnpm dlx publint
- run: pnpm dlx @arethetypeswrong/cli --pack .
```
# CI Workflows
## Basic 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
```
## Matrix Testing
```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
```
## Skip Docs-Only Changes
```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'
# ... rest of job
```
## Auto-fix Commits
```yaml
- run: pnpm lint:fix
- uses: stefanzweifel/git-auto-commit-action@v5
if: github.event_name == 'push'
with:
commit_message: 'chore: lint fix'
```
## Release on Tag (Token-based)
```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 }}
```
## Release on Tag (OIDC - Recommended)
No NPM_TOKEN needed. Uses GitHub OIDC for tokenless auth with 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 # Required: 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 Setup Steps
1. Open `https://www.npmjs.com/package/<PACKAGE_NAME>/access`
2. Scroll to "Publishing access" section
3. Click "Add GitHub Actions" under Trusted Publishers
4. Fill: Owner, Repository, Workflow file (`release.yml`), Environment (empty)
5. Click "Add"
### OIDC Requirements
1. **Node.js 24+** (npm 11.5.1+ required - Node 22 has npm 10.x which fails)
2. **Permissions**: `id-token: write`
3. **Publish flag**: `--provenance`
4. **package.json**: must have `repository` field
5. **npm 2FA**: "Require 2FA or granular access token" (allows OIDC)
### Troubleshooting
| Error | Cause | Fix |
| ------------------------------------- | -------------------- | ----------------------------------------- |
| "Access token expired" E404 | npm too old | Use Node.js 24 |
| ENEEDAUTH | Missing registry-url | Add `registry-url` to setup-node |
| "repository.url is empty" E422 | Missing field | Add `repository` to package.json |
| "not configured as trusted publisher" | Config mismatch | Check owner, repo, workflow match exactly |
## Monorepo Matrix
```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
```
## Concurrency Control
Cancel outdated runs:
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
```
## pkg-pr-new for PRs
```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
```
## Package Validation in CI
```yaml
- run: pnpm build
- run: pnpm dlx publint
- run: pnpm dlx @arethetypeswrong/cli --pack .
```
@@ -1,180 +1,180 @@
# Release Workflow
## Tools
| Tool | Purpose |
| ----------- | --------------------------------- |
| bumpp | Interactive version bumping |
| changelogen | Changelog generation from commits |
| pkg-pr-new | PR preview packages |
## bumpp (Version Bumping)
```bash
pnpm add -D bumpp
```
```json
{
"scripts": {
"release": "bumpp"
}
}
```
Interactive prompt for patch/minor/major. Options:
```json
{
"scripts": {
"release": "bumpp --commit --tag --push"
}
}
```
For monorepos:
```bash
bumpp -r # Recursive
bumpp packages/*/package.json # Specific packages
```
## changelogen (Changelog)
```bash
pnpm add -D changelogen
```
```json
{
"scripts": {
"changelog": "changelogen --release"
}
}
```
Combined workflow:
```json
{
"scripts": {
"release": "changelogen --release && bumpp"
}
}
```
## Full Release Flow
```json
{
"scripts": {
"release": "pnpm lint && pnpm test && changelogen --release && bumpp --commit --tag --push"
}
}
```
CI publishes to npm on tag push.
## pkg-pr-new (PR Previews)
For publishable packages. Creates install links on PRs.
```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
```
For monorepos:
```bash
pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*'
```
PR comment shows:
```
pnpm add https://pkg.pr.new/your-org/your-package@123
```
## Conventional Commits
For changelogen to work:
```
feat: add dark mode support
fix: resolve memory leak in parser
docs: update README
chore: update dependencies
```
## npm Publishing
### Token-based (legacy)
```yaml
- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### OIDC (Recommended)
No token needed. See ci-workflows.md for full setup.
```yaml
- run: pnpm publish --access public --no-git-checks --provenance
```
## Monorepo Publishing
With pnpm:
```bash
pnpm -r publish --access public
```
With bumpp:
```bash
bumpp -r && pnpm -r publish
```
## Pre-release Versions
```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 Requirements
```json
{
"name": "@scope/package",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/org/repo.git"
},
"publishConfig": {
"access": "public"
}
}
```
`repository` required for npm provenance.
# Release Workflow
## Tools
| Tool | Purpose |
| ----------- | --------------------------------- |
| bumpp | Interactive version bumping |
| changelogen | Changelog generation from commits |
| pkg-pr-new | PR preview packages |
## bumpp (Version Bumping)
```bash
pnpm add -D bumpp
```
```json
{
"scripts": {
"release": "bumpp"
}
}
```
Interactive prompt for patch/minor/major. Options:
```json
{
"scripts": {
"release": "bumpp --commit --tag --push"
}
}
```
For monorepos:
```bash
bumpp -r # Recursive
bumpp packages/*/package.json # Specific packages
```
## changelogen (Changelog)
```bash
pnpm add -D changelogen
```
```json
{
"scripts": {
"changelog": "changelogen --release"
}
}
```
Combined workflow:
```json
{
"scripts": {
"release": "changelogen --release && bumpp"
}
}
```
## Full Release Flow
```json
{
"scripts": {
"release": "pnpm lint && pnpm test && changelogen --release && bumpp --commit --tag --push"
}
}
```
CI publishes to npm on tag push.
## pkg-pr-new (PR Previews)
For publishable packages. Creates install links on PRs.
```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
```
For monorepos:
```bash
pnpm dlx pkg-pr-new publish --compact --pnpm './packages/*'
```
PR comment shows:
```
pnpm add https://pkg.pr.new/your-org/your-package@123
```
## Conventional Commits
For changelogen to work:
```
feat: add dark mode support
fix: resolve memory leak in parser
docs: update README
chore: update dependencies
```
## npm Publishing
### Token-based (legacy)
```yaml
- run: pnpm publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
### OIDC (Recommended)
No token needed. See ci-workflows.md for full setup.
```yaml
- run: pnpm publish --access public --no-git-checks --provenance
```
## Monorepo Publishing
With pnpm:
```bash
pnpm -r publish --access public
```
With bumpp:
```bash
bumpp -r && pnpm -r publish
```
## Pre-release Versions
```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 Requirements
```json
{
"name": "@scope/package",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/org/repo.git"
},
"publishConfig": {
"access": "public"
}
}
```
`repository` required for npm provenance.
@@ -1,201 +1,201 @@
# Testing
## Vitest Setup
```bash
pnpm add -D vitest
```
### Basic Config
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
testTimeout: 30_000,
reporters: 'dot',
},
})
```
### With Coverage
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/types.ts'],
reporter: ['text', 'lcovonly', 'html'],
},
},
})
```
## Workspace Projects
For monorepos, test packages separately:
```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-Based Testing
Test transforms with file fixtures:
```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()
})
}
})
```
## Idempotency Testing
Ensure transforms are stable:
```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) // Should not change
})
```
## Type-Level Testing
Test TypeScript types:
```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 }>()
})
})
```
## Multi-TS Version Testing
Test across TypeScript versions (TanStack pattern):
```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
```
## Package Validation
Validate published package:
```bash
# Check exports are correct
pnpm dlx publint
# Check types work in different moduleResolutions
pnpm dlx @arethetypeswrong/cli --pack .
```
Add to tsdown config:
```typescript
export default defineConfig({
attw: { profile: 'esm-only' }, // or 'node16'
})
```
## Test Scripts
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:types": "vitest typecheck"
}
}
```
## Mocking
```typescript
import { vi } from 'vitest'
vi.mock('fs', () => ({
readFileSync: vi.fn(() => 'mocked content'),
}))
// Spy on method
const spy = vi.spyOn(console, 'log')
expect(spy).toHaveBeenCalledWith('expected')
```
## Testing Plugins
Dogfood your own plugin in tests:
```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'],
},
})
```
# Testing
## Vitest Setup
```bash
pnpm add -D vitest
```
### Basic Config
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/**/*.test.ts'],
testTimeout: 30_000,
reporters: 'dot',
},
})
```
### With Coverage
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/types.ts'],
reporter: ['text', 'lcovonly', 'html'],
},
},
})
```
## Workspace Projects
For monorepos, test packages separately:
```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-Based Testing
Test transforms with file fixtures:
```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()
})
}
})
```
## Idempotency Testing
Ensure transforms are stable:
```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) // Should not change
})
```
## Type-Level Testing
Test TypeScript types:
```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 }>()
})
})
```
## Multi-TS Version Testing
Test across TypeScript versions (TanStack pattern):
```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
```
## Package Validation
Validate published package:
```bash
# Check exports are correct
pnpm dlx publint
# Check types work in different moduleResolutions
pnpm dlx @arethetypeswrong/cli --pack .
```
Add to tsdown config:
```typescript
export default defineConfig({
attw: { profile: 'esm-only' }, // or 'node16'
})
```
## Test Scripts
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:types": "vitest typecheck"
}
}
```
## Mocking
```typescript
import { vi } from 'vitest'
vi.mock('fs', () => ({
readFileSync: vi.fn(() => 'mocked content'),
}))
// Spy on method
const spy = vi.spyOn(console, 'log')
expect(spy).toHaveBeenCalledWith('expected')
```
## Testing Plugins
Dogfood your own plugin in tests:
```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'],
},
})
```