返回 Skills
antfu/skills· MIT 内容可用

antfu

Anthony Fu's opinionated tooling and conventions for JavaScript/TypeScript projects. Use when setting up new projects, configuring ESLint/Prettier alternatives, monorepos, library publishing, or when the user mentions Anthony Fu's preferences.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: antfu description: Anthony Fu's opinionated tooling and conventions for JavaScript/TypeScript projects. Use when setting up new projects, configuring ESLint/Prettier alternatives, monorepos, library publishing, or when the user mentions Anthony Fu's preferences. metadata: author: Anthony Fu version: "2026.06.22"

Coding Practices

Code Organization

  • Single responsibility: Each source file should have a clear, focused scope/purpose
  • Split large files: Break files when they become large or handle too many concerns
  • Type separation: Always separate types and interfaces into types.ts or types/*.ts
  • Constants extraction: Move constants to a dedicated constants.ts file

Runtime Environment

  • Prefer isomorphic code: Write runtime-agnostic code that works in Node, browser, and workers whenever possible
  • Clear runtime indicators: When code is environment-specific, add a comment at the top of the file:
// @env node
// @env browser

TypeScript

  • Explicit return types: Declare return types explicitly when possible
  • Avoid complex inline types: Extract complex types into dedicated type or interface declarations

Explicitness

Favor explicit, traceable code over implicit "magic". A reader (human or agent) should be able to follow where every name comes from without running tooling.

  • Explicit imports: Prefer explicit import statements. Avoid auto-imports — when a framework provides them (e.g. Nuxt/Nitro), turn them off for new projects (see app-development).
  • No path aliases by default: Use relative imports (./foo, ../bar). Only use path aliases (@/, ~/, #imports, etc.) when they are already configured in the project; don't introduce new ones for greenfield code.

Comments

  • Avoid unnecessary comments: Code should be self-explanatory
  • Explain "why" not "how": Comments should describe the reasoning or intent, not what the code does

Testing (Vitest)

  • Test files: foo.tsfoo.test.ts (same directory)
  • Use describe/it API (not test)
  • Use toMatchSnapshot for complex outputs
  • Use toMatchFileSnapshot with explicit path for language-specific snapshots

Tooling Choices

@antfu/ni Commands

CommandDescription
niInstall dependencies
ni <pkg> / ni -D <pkg>Add dependency / dev dependency
nr <script>Run script
nuUpgrade dependencies
nun <pkg>Uninstall dependency
nciClean install (pnpm i --frozen-lockfile)
nlx <pkg>Execute package (npx)

Checking npm Package Versions

Use fast-npm-meta to look up the latest version of a package — it queries a small metadata endpoint instead of downloading the full registry payload (which can be megabytes per package).

nlx fast-npm-meta version vite              # 7.3.1
nlx fast-npm-meta version "nuxt@^3.5"       # 3.5.22 — range-aware
nlx fast-npm-meta version vite nuxt vue     # multiple at once
nlx fast-npm-meta version vite --json       # JSON for scripting
nlx fast-npm-meta full vite                 # full version list + dist-tags

Prefer this over npm view <pkg> version when you only need the latest version, and over reading package.json from the registry directly.

TypeScript Config

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  }
}

ESLint Setup

// eslint.config.mjs
import antfu from '@antfu/eslint-config'

export default antfu()

When completing tasks, run pnpm run lint --fix to format the code and fix coding style.

For detailed configuration options: antfu-eslint-config

Git Hooks

{
  "simple-git-hooks": {
    "pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged"
  },
  "lint-staged": { "*": "eslint --fix" },
  "scripts": {
    "prepare": "npx simple-git-hooks"
  }
}

pnpm Catalogs

Use named catalogs in pnpm-workspace.yaml for version management:

CatalogPurpose
prodProduction dependencies
inlinedBundler-inlined dependencies
devDev tools (linter, bundler, testing)
frontendFrontend libraries

Avoid the default catalog. Catalog names can be adjusted per project needs.


References

TopicDescriptionReference
ESLint ConfigFramework support, formatters, rule overrides, VS Code settingsantfu-eslint-config
Project Setup.gitignore, GitHub Actions, VS Code extensionssetting-up
App DevelopmentVue/Nuxt/UnoCSS conventions, auto-import control, Storybook component testingapp-development
Library Developmenttsdown bundling, pure ESM publishinglibrary-development
Monorepopnpm workspaces, centralized alias, Turborepomonorepo

附带文件

references/antfu-eslint-config.md
---
name: antfu-eslint-config
description: Configuring @antfu/eslint-config for framework support, formatters, and rule overrides. Use when adding React/Vue/Svelte/Astro support, customizing rules, or setting up VS Code integration.
---

# @antfu/eslint-config

Handles both linting and formatting (no Prettier needed). Auto-detects TypeScript and Vue.

**Style**: Single quotes, no semicolons, sorted imports, dangling commas.

## Configuration Options

```js
import antfu from '@antfu/eslint-config'

export default antfu({
  // Project type: 'lib' for libraries, 'app' (default) for applications
  type: 'lib',

  // Global ignores (extends defaults, doesn't override)
  ignores: ['**/fixtures', '**/dist'],

  // Stylistic options
  stylistic: {
    indent: 2,        // 2, 4, or 'tab'
    quotes: 'single', // or 'double'
  },

  // Framework support (auto-detected, but can be explicit)
  typescript: true,
  vue: true,

  // Disable specific language support
  jsonc: false,
  yaml: false,
})
```

## Framework Support

### Vue

Vue accessibility:

```js
export default antfu({
  vue: {
    a11y: true
  },
})
// Requires: pnpm add -D eslint-plugin-vuejs-accessibility
```

### React

```js
export default antfu({
  react: true,
})
// Requires: pnpm add -D @eslint-react/eslint-plugin eslint-plugin-react-hooks eslint-plugin-react-refresh
```

### Next.js

```js
export default antfu({
  nextjs: true,
})
// Requires: pnpm add -D @next/eslint-plugin-next
```

### Svelte

```js
export default antfu({
  svelte: true,
})
// Requires: pnpm add -D eslint-plugin-svelte
```

### Astro

```js
export default antfu({
  astro: true,
})
// Requires: pnpm add -D eslint-plugin-astro
```

### Solid

```js
export default antfu({
  solid: true,
})
// Requires: pnpm add -D eslint-plugin-solid
```

### UnoCSS

```js
export default antfu({
  unocss: true,
})
// Requires: pnpm add -D @unocss/eslint-plugin
```

## Formatters (CSS, HTML, Markdown)

For files ESLint doesn't handle natively:

```js
export default antfu({
  formatters: {
    css: true,      // Format CSS, LESS, SCSS (uses Prettier)
    html: true,     // Format HTML (uses Prettier)
    markdown: 'prettier' // or 'dprint'
  }
})
// Requires: pnpm add -D eslint-plugin-format
```

## Rule Overrides

### Global overrides

```js
export default antfu(
  {
    // First argument: antfu config options
  },
  // Additional arguments: ESLint flat configs
  {
    rules: {
      'style/semi': ['error', 'never'],
    },
  }
)
```

### Per-integration overrides

```js
export default antfu({
  vue: {
    overrides: {
      'vue/operator-linebreak': ['error', 'before'],
    },
  },
  typescript: {
    overrides: {
      'ts/consistent-type-definitions': ['error', 'interface'],
    },
  },
})
```

### File-specific overrides

```js
export default antfu(
  { vue: true, typescript: true },
  {
    files: ['**/*.vue'],
    rules: {
      'vue/operator-linebreak': ['error', 'before'],
    },
  }
)
```

## Plugin Prefix Renaming

The config renames plugin prefixes for consistency:

| New Prefix | Original |
|------------|----------|
| `ts/*` | `@typescript-eslint/*` |
| `style/*` | `@stylistic/*` |
| `import/*` | `import-lite/*` |
| `node/*` | `n/*` |
| `yaml/*` | `yml/*` |
| `test/*` | `vitest/*` |
| `next/*` | `@next/next` |

Use the new prefix when overriding or disabling rules:

```ts
// eslint-disable-next-line ts/consistent-type-definitions
type Foo = { bar: 2 }
```

## Type-Aware Rules

Enable TypeScript type checking:

```js
export default antfu({
  typescript: {
    tsconfigPath: 'tsconfig.json',
  },
})
```

## Config Composer API

Chain methods for flexible composition:

```js
export default antfu()
  .prepend(/* configs before main */)
  .override('antfu/stylistic/rules', {
    rules: {
      'style/generator-star-spacing': ['error', { after: true, before: false }],
    }
  })
  .renamePlugins({
    'old-prefix': 'new-prefix',
  })
```

## Less Opinionated Mode

Disable Anthony's most opinionated rules:

```js
export default antfu({
  lessOpinionated: true
})
```

## Lint-Staged Setup

```json
{
  "simple-git-hooks": {
    "pre-commit": "pnpm lint-staged"
  },
  "lint-staged": {
    "*": "eslint --fix"
  }
}
```

```bash
pnpm add -D lint-staged simple-git-hooks
npx simple-git-hooks
```

## VS Code Settings

Add to `.vscode/settings.json`:

```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 },
    { "rule": "*-spaces", "severity": "off", "fixable": true },
    { "rule": "*-order", "severity": "off", "fixable": true },
    { "rule": "*-dangle", "severity": "off", "fixable": true },
    { "rule": "*-newline", "severity": "off", "fixable": true },
    { "rule": "*quotes", "severity": "off", "fixable": true },
    { "rule": "*semi", "severity": "off", "fixable": true }
  ],
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "markdown",
    "json",
    "jsonc",
    "yaml",
    "toml",
    "xml",
    "astro",
    "svelte",
    "css",
    "less",
    "scss"
  ]
}
```

<!-- 
Source references:
- https://github.com/antfu/eslint-config
- https://raw.githubusercontent.com/antfu/eslint-config/refs/heads/main/README.md
-->
references/app-development.md
---
name: app-development
description: Vue/Nuxt/UnoCSS application conventions. Use when building web apps, choosing between Vite and Nuxt, or writing Vue components.
---

# App Development

## Framework Selection

| Use Case | Choice |
|----------|--------|
| SPA, client-only, library playgrounds | Vite + Vue |
| SSR, SSG, SEO-critical, file-based routing, API routes | Nuxt |

## Nuxt Conventions

### Disable Auto-imports (new projects)

Prefer explicit imports over auto-imports so every symbol is traceable. For new Nuxt projects, turn off both app-side (Nuxt) and server-side (Nitro) auto-imports in `nuxt.config.ts`:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  imports: {
    autoImport: false, // disable composable/util auto-imports
  },
  components: {
    dirs: [], // disable component auto-imports
  },
  nitro: {
    imports: false, // disable server-side (Nitro) auto-imports
  },
})
```

Framework helpers stay available through the `#imports` alias — import them explicitly:

```ts
import { computed, ref } from '#imports'
```

| Option | Effect |
|--------|--------|
| `imports.autoImport: false` | Stops auto-importing `~/composables` and `~/utils` (and framework APIs like `ref`) |
| `components.dirs: []` | Stops auto-importing components from `~/components` |
| `nitro.imports: false` | Stops auto-importing in the server (`server/utils`, etc.) |

> Standalone Nitro projects already default to `imports: false` — leave server auto-imports off rather than enabling them.

### Path Aliases

Nuxt's built-in aliases (`~/`, `@/`, `#imports`) are already configured, so they're fine to use. Don't add custom path aliases for new code — prefer relative imports otherwise.

## Vue Conventions

| Convention | Preference |
|------------|------------|
| Script syntax | Always `<script setup lang="ts">` |
| State | Prefer `shallowRef()` over `ref()` |
| Objects | Use `ref()`, avoid `reactive()` |
| Styling | UnoCSS |
| Utilities | VueUse |

### Props and Emits

```vue
<script setup lang="ts">
interface Props {
  title: string
  count?: number
}

interface Emits {
  (e: 'update', value: number): void
  (e: 'close'): void
}

const props = withDefaults(defineProps<Props>(), {
  count: 0,
})

const emit = defineEmits<Emits>()
</script>
```

### Storybook for Components

Set up Storybook for component development. Expressing each state as a story keeps components side-effect-free and their states predictable.

Run the story tests in CI. Prefer the Vitest addon (`@storybook/addon-vitest`) so stories run as part of the existing `vitest` run (see [setting-up](setting-up.md)).
references/library-development.md
---
name: library-development
description: Building and publishing TypeScript libraries with tsdown. Use when creating npm packages, configuring library bundling, or setting up package.json exports.
---

# Library Development

| Aspect | Choice |
|--------|--------|
| Bundler | tsdown |
| Output | Pure ESM only (no CJS) |
| DTS | Generated via tsdown |
| Exports | Auto-generated via tsdown |

## tsdown Configuration

Use tsdown with these options enabled:

```ts
// tsdown.config.ts
import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm'],
  dts: true,
  exports: true,
})
```

| Option | Value | Purpose |
|--------|-------|---------|
| `format` | `['esm']` | Pure ESM, no CommonJS |
| `dts` | `true` | Generate `.d.ts` files |
| `exports` | `true` | Auto-update `exports` field in `package.json` |

### Multiple Entry Points

```ts
export default defineConfig({
  entry: [
    'src/index.ts',
    'src/utils.ts',
  ],
  format: ['esm'],
  dts: true,
  exports: true,
})
```

The `exports: true` option auto-generates the `exports` field in `package.json` when running `tsdown`.

---

## API Stability

For published libraries, lock the public API surface so accidental breaking changes appear as a diff in code review.

| Tool | Purpose |
|------|---------|
| [`tsnapi`](https://github.com/antfu/tsnapi) | Snapshots runtime exports + type declarations into committed `.snapshot.js` / `.snapshot.d.ts` files via Vitest |
| [`tsdown-stale-guard`](https://github.com/antfu-collective/tsdown-stale-guard) | Records build input/output hashes so tests fail fast when run against a stale build |

Install both as dev dependencies. Wire `tsdown-stale-guard` as a tsdown plugin so every build records its hash:

```ts
// tsdown.config.ts
import { defineConfig } from 'tsdown'
import { StaleGuardRecorder } from 'tsdown-stale-guard'

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm'],
  dts: true,
  exports: true,
  plugins: [
    StaleGuardRecorder(),
  ],
})
```

Snapshot the public API from a Vitest test — prefer the Vitest helpers over the tsdown/Rolldown plugin so build config stays focused on building. Each file gates itself on a fresh build via a per-file `beforeAll(guardStaleBuild)` so the committed `dist/` can never drift from source:

```ts
// test/api.test.ts (single package)
import { beforeAll } from 'vitest'
import { snapshotApiPerEntry } from 'tsnapi/vitest'
import { guardStaleBuild } from 'tsdown-stale-guard'

beforeAll(async () => {
  await guardStaleBuild()
})

await snapshotApiPerEntry(new URL('..', import.meta.url).pathname)
```

For monorepos, wrap `describePackagesApiSnapshots()` in a `describe` so the per-package suites share a single stale-build gate:

```ts
// test/api.test.ts (monorepo, run from root)
import { beforeAll, describe } from 'vitest'
import { describePackagesApiSnapshots } from 'tsnapi/vitest'
import { guardStaleBuild } from 'tsdown-stale-guard'

describe('packages api', async () => {
  beforeAll(async () => {
    await guardStaleBuild()
  })

  await describePackagesApiSnapshots()
})
```

### Updating snapshots

After an intentional API change, rebuild and update both snapshots in one go:

```bash
nr build           # regenerates dist/ and the stale-guard hash
nr test -u         # updates the .snapshot.js / .snapshot.d.ts files
```

---

## package.json

Required fields for pure ESM library:

```json
{
  "type": "module",
  "main": "./dist/index.mjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.mts",
  "files": ["dist"],
  "scripts": {
    "build": "tsdown",
    "prepack": "pnpm build",
    "test": "vitest",
    "release": "bumpp -r"
  }
}
```

The `exports` field is managed by tsdown when `exports: true`.

### prepack Script

For each public package, add `"prepack": "pnpm build"` to `scripts`. This ensures the package is automatically built before publishing (e.g., when running `npm publish` or `pnpm publish`). This prevents accidentally publishing stale or missing build artifacts.
references/monorepo.md
---
name: monorepo
description: Monorepo setup with pnpm workspaces, centralized aliases, and Turborepo. Use when creating or managing multi-package repositories.
---

# Monorepo Setup

## pnpm Workspaces

Use pnpm workspaces for monorepo management:

```yaml
# pnpm-workspace.yaml
packages:
  - 'packages/*'
```

## Scripts Convention

Have scripts in each package, and use `-r` (recursive) flag at root,
Enable ESLint cache for faster linting in monorepos.

```json
// root package.json
{
  "scripts": {
    "build": "pnpm run -r build",
    "test": "vitest",
    "lint": "eslint . --cache --concurrency=auto"
  }
}
```

In each package's `package.json`, add the scripts.

```json
// packages/*/package.json
{
  "scripts": {
    "build": "tsdown",
    "prepack": "pnpm build"
  }
}
```

## ESLint Cache


```json
{
  "scripts": {
    "lint": "eslint . --cache --concurrency=auto"
  }
}
```

## Turborepo (Optional)

For monorepos with many packages or long build times, use Turborepo for task orchestration and caching.

See the dedicated Turborepo skill for detailed configuration.

## Centralized Alias

Path aliases should be explicit and centralized — this is the deliberate "already set up" case. Outside of these configured aliases, prefer relative imports; don't sprinkle ad-hoc aliases across the codebase.

For better DX across Vite, Nuxt, Vitest configs, create a centralized `alias.ts` at project root:

```ts
// alias.ts
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { join, relative } from 'pathe'

const root = fileURLToPath(new URL('.', import.meta.url))
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url))

export const alias = {
  '@myorg/core': r('core/src/index.ts'),
  '@myorg/utils': r('utils/src/index.ts'),
  '@myorg/ui': r('ui/src/index.ts'),
  // Add more aliases as needed
}

// Auto-update tsconfig.alias.json paths
const raw = fs.readFileSync(join(root, 'tsconfig.alias.json'), 'utf-8').trim()
const tsconfig = JSON.parse(raw)
tsconfig.compilerOptions.paths = Object.fromEntries(
  Object.entries(alias).map(([key, value]) => [key, [`./${relative(root, value)}`]]),
)
const newRaw = JSON.stringify(tsconfig, null, 2)
if (newRaw !== raw)
  fs.writeFileSync(join(root, 'tsconfig.alias.json'), `${newRaw}\n`, 'utf-8')
```

Then update the `tsconfig.json` to use the alias file:

```json
{
  "extends": [
    "./tsconfig.alias.json"
  ]
}
```

### Using Alias in Configs

Reference the centralized alias in all config files:

```ts
// vite.config.ts
import { alias } from './alias'

export default defineConfig({
  resolve: { alias },
})
```

```ts
// nuxt.config.ts
import { alias } from './alias'

export default defineNuxtConfig({
  alias,
})
```
references/setting-up.md
---
name: setting-up
description: Project setup files including .gitignore, GitHub Actions workflows, and VS Code extensions. Use when initializing new projects or adding CI/editor config.
---

# Project Setup

## .gitignore

Create when `.gitignore` is not present:

```
*.log
*.tgz
.cache
.DS_Store
.eslintcache
.idea
.env
.nuxt
.temp
.output
.turbo
cache
coverage
dist
lib-cov
logs
node_modules
temp
```

## GitHub Actions

Add these workflows when setting up a new project. Skip if workflows already exist. All use [sxzz/workflows](https://github.com/sxzz/workflows) reusable workflows.

### Autofix Workflow

**`.github/workflows/autofix.yml`** - Auto-fix linting on PRs:

```yaml
name: autofix.ci

on: [pull_request]

jobs:
  autofix:
    uses: sxzz/workflows/.github/workflows/autofix.yml@main
    permissions:
      contents: read
```

### Unit Test Workflow

**`.github/workflows/unit-test.yml`** - Run tests on push/PR:

```yaml
name: Unit Test

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions: {}

jobs:
  unit-test:
    uses: sxzz/workflows/.github/workflows/unit-test.yml@main
```

### Release Workflow

**`.github/workflows/release.yml`** - Publish on tag (library projects only):

```yaml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    uses: sxzz/workflows/.github/workflows/release.yml@main
    with:
      publish: true
    permissions:
      contents: write
      id-token: write
```

## VS Code Extensions

Configure in `.vscode/extensions.json`:

```json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "antfu.pnpm-catalog-lens",
    "antfu.iconify",
    "antfu.unocss",
    "antfu.slidev",
    "vue.volar"
  ]
}
```

| Extension | Description |
|-----------|-------------|
| `dbaeumer.vscode-eslint` | ESLint integration for linting and formatting |
| `antfu.pnpm-catalog-lens` | Shows pnpm catalog version hints inline |
| `antfu.iconify` | Iconify icon preview and autocomplete |
| `antfu.unocss` | UnoCSS IntelliSense and syntax highlighting |
| `antfu.slidev` | Slidev preview and syntax highlighting |
| `vue.volar` | Vue Language Features |