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:
@@ -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'],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user