chore(.agents/skills): added more useful skills, remove old one

This commit is contained in:
Neko Ayaka
2026-08-04 19:24:11 +08:00
parent 04485788d0
commit 55bd16e175
70 changed files with 12860 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/pinia`
- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e`
- **Generated:** 2026-01-28
+59
View File
@@ -0,0 +1,59 @@
---
name: pinia
description: Pinia official Vue state management library, type-safe and extensible. Use when defining stores, working with state/getters/actions, or implementing store patterns in Vue apps.
metadata:
author: Anthony Fu
version: "2026.1.28"
source: Generated from https://github.com/vuejs/pinia, scripts located at https://github.com/antfu/skills
---
# Pinia
Pinia is the official state management library for Vue, designed to be intuitive and type-safe. It supports both Options API and Composition API styles, with first-class TypeScript support and devtools integration.
> The skill is based on Pinia v3.0.4, generated at 2026-01-28.
## Core References
| Topic | Description | Reference |
|-------|-------------|-----------|
| Stores | Defining stores, state, getters, actions, storeToRefs, subscriptions | [core-stores](references/core-stores.md) |
## Features
### Extensibility
| Topic | Description | Reference |
|-------|-------------|-----------|
| Plugins | Extend stores with custom properties, state, and behavior | [features-plugins](references/features-plugins.md) |
### Composability
| Topic | Description | Reference |
|-------|-------------|-----------|
| Composables | Using Vue composables within stores (VueUse, etc.) | [features-composables](references/features-composables.md) |
| Composing Stores | Store-to-store communication, avoiding circular dependencies | [features-composing-stores](references/features-composing-stores.md) |
## Best Practices
| Topic | Description | Reference |
|-------|-------------|-----------|
| Testing | Unit testing with @pinia/testing, mocking, stubbing | [best-practices-testing](references/best-practices-testing.md) |
| Outside Components | Using stores in navigation guards, plugins, middlewares | [best-practices-outside-component](references/best-practices-outside-component.md) |
## Advanced
| Topic | Description | Reference |
|-------|-------------|-----------|
| SSR | Server-side rendering, state hydration | [advanced-ssr](references/advanced-ssr.md) |
| Nuxt | Nuxt integration, auto-imports, SSR best practices | [advanced-nuxt](references/advanced-nuxt.md) |
| HMR | Hot module replacement for development | [advanced-hmr](references/advanced-hmr.md) |
## Key Recommendations
- **Prefer Setup Stores** for complex logic, composables, and watchers
- **Use `storeToRefs()`** when destructuring state/getters to preserve reactivity
- **Actions can be destructured directly** - they're bound to the store
- **Call stores inside functions** not at module scope, especially for SSR
- **Add HMR support** to each store for better development experience
- **Use `@pinia/testing`** for component tests with mocked stores
@@ -0,0 +1,61 @@
---
name: hot-module-replacement
description: Enable HMR to preserve store state during development
---
# Hot Module Replacement (HMR)
Pinia supports HMR to edit stores without page reload, preserving existing state.
## Setup
Add this snippet after each store definition:
```ts
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useAuth = defineStore('auth', {
// store options...
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))
}
```
## Setup Store Example
```ts
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
}
```
## Bundler Support
- **Vite:** Officially supported via `import.meta.hot`
- **Webpack:** Uses `import.meta.webpackHot`
- Any bundler implementing the `import.meta.hot` spec should work
## Nuxt
With `@pinia/nuxt`, `acceptHMRUpdate` is auto-imported but you still need to add the HMR snippet manually.
## Benefits
- Edit store logic without losing state
- Add/remove state, actions, and getters on the fly
- Faster development iteration
<!--
Source references:
- https://pinia.vuejs.org/cookbook/hot-module-replacement.html
-->
@@ -0,0 +1,119 @@
---
name: nuxt-integration
description: Using Pinia with Nuxt - auto-imports, SSR, and best practices
---
# Nuxt Integration
Pinia works seamlessly with Nuxt 3/4, handling SSR, serialization, and XSS protection automatically.
## Installation
```bash
npx nuxi@latest module add pinia
```
This installs both `@pinia/nuxt` and `pinia`. If `pinia` isn't installed, add it manually.
> **npm users:** If you get `ERESOLVE unable to resolve dependency tree`, add to `package.json`:
> ```json
> "overrides": { "vue": "latest" }
> ```
## Configuration
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
```
## Auto Imports
These are automatically available:
- `usePinia()` - get pinia instance
- `defineStore()` - define stores
- `storeToRefs()` - extract reactive refs
- `acceptHMRUpdate()` - HMR support
**All stores in `app/stores/` (Nuxt 4) or `stores/` are auto-imported.**
### Custom Store Directories
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
storesDirs: ['./stores/**', './custom-folder/stores/**'],
},
})
```
## Fetching Data in Pages
Use `callOnce()` for SSR-friendly data fetching:
```vue
<script setup>
const store = useStore()
// Run once, data persists across navigations
await callOnce('user', () => store.fetchUser())
</script>
```
### Refetch on Navigation
```vue
<script setup>
const store = useStore()
// Refetch on every navigation (like useFetch)
await callOnce('user', () => store.fetchUser(), { mode: 'navigation' })
</script>
```
## Using Stores Outside Components
In navigation guards, middlewares, or other stores, pass the `pinia` instance:
```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const nuxtApp = useNuxtApp()
const store = useStore(nuxtApp.$pinia)
if (to.meta.requiresAuth && !store.isLoggedIn) {
return navigateTo('/login')
}
})
```
Most of the time, you don't need this - just use stores in components or other injection-aware contexts.
## Pinia Plugins with Nuxt
Create a Nuxt plugin:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`)
})
return { creationTime: new Date() }
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})
```
<!--
Source references:
- https://pinia.vuejs.org/ssr/nuxt.html
-->
@@ -0,0 +1,121 @@
---
name: server-side-rendering
description: SSR setup, state hydration, and avoiding cross-request state pollution
---
# Server Side Rendering (SSR)
Pinia works with SSR when stores are called at the top of `setup`, getters, or actions.
> **Using Nuxt?** See the [Nuxt integration](advanced-nuxt.md) instead.
## Basic Usage
```vue
<script setup>
// ✅ Works - pinia knows the app context in setup
const main = useMainStore()
</script>
```
## Using Store Outside setup()
Pass the `pinia` instance explicitly:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
router.beforeEach((to) => {
// ✅ Pass pinia for correct SSR context
const main = useMainStore(pinia)
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login'
}
})
```
## serverPrefetch()
Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
return store.fetchData()
},
}
```
## onServerPrefetch()
Works normally:
```vue
<script setup>
const store = useStore()
onServerPrefetch(async () => {
await store.fetchData()
})
</script>
```
## State Hydration
Serialize state on server and hydrate on client.
### Server Side
Use [devalue](https://github.com/Rich-Harris/devalue) for XSS-safe serialization:
```ts
import devalue from 'devalue'
import { createPinia } from 'pinia'
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
// After rendering, state is available
const serializedState = devalue(pinia.state.value)
// Inject into HTML as global variable
```
### Client Side
Hydrate before any `useStore()` call:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// Hydrate from serialized state (e.g., from window.__pinia)
if (typeof window !== 'undefined') {
pinia.state.value = JSON.parse(window.__pinia)
}
```
## SSR Examples
- [Vitesse template](https://github.com/antfu/vitesse/blob/main/src/modules/pinia.ts)
- [vite-plugin-ssr](https://vite-plugin-ssr.com/pinia)
## Key Points
1. Call stores inside functions, not at module scope
2. Pass `pinia` instance when using stores outside components in SSR
3. Hydrate state before calling any `useStore()`
4. Use `devalue` or similar for safe serialization
5. Avoid cross-request state pollution by creating fresh pinia per request
<!--
Source references:
- https://pinia.vuejs.org/ssr/
-->
@@ -0,0 +1,115 @@
---
name: using-stores-outside-components
description: Correctly using stores in navigation guards, plugins, and other non-component contexts
---
# Using Stores Outside Components
Stores need the `pinia` instance, which is automatically injected in components. Outside components, you may need to provide it manually.
## Single Page Applications
Call stores **after** pinia is installed:
```ts
import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
// ❌ Fails - pinia not created yet
const userStore = useUserStore()
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// ✅ Works - pinia is active
const userStore = useUserStore()
```
## Navigation Guards
**Wrong:** Call at module level
```ts
import { createRouter } from 'vue-router'
const router = createRouter({ /* ... */ })
// ❌ May fail depending on import order
const store = useUserStore()
router.beforeEach((to) => {
if (store.isLoggedIn) { /* ... */ }
})
```
**Correct:** Call inside the guard
```ts
router.beforeEach((to) => {
// ✅ Called after pinia is installed
const store = useUserStore()
if (to.meta.requiresAuth && !store.isLoggedIn) {
return '/login'
}
})
```
## SSR Applications
Always pass the `pinia` instance to `useStore()`:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
router.beforeEach((to) => {
// ✅ Pass pinia instance
const main = useMainStore(pinia)
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login'
}
})
```
## serverPrefetch()
Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
return store.fetchData()
},
}
```
## onServerPrefetch()
Works normally in `<script setup>`:
```vue
<script setup>
const store = useStore()
onServerPrefetch(async () => {
// ✅ Just works
await store.fetchData()
})
</script>
```
## Key Takeaway
Defer `useStore()` calls to functions that run after pinia is installed, rather than calling at module scope.
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/outside-component-usage.html
-->
@@ -0,0 +1,212 @@
---
name: testing
description: Unit testing stores and components with @pinia/testing
---
# Testing Stores
## Unit Testing Stores
Create a fresh pinia instance for each test:
```ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('increments', () => {
const counter = useCounterStore()
expect(counter.n).toBe(0)
counter.increment()
expect(counter.n).toBe(1)
})
})
```
### With Plugins
```ts
import { setActivePinia, createPinia } from 'pinia'
import { createApp } from 'vue'
import { somePlugin } from '../src/stores/plugin'
const app = createApp({})
beforeEach(() => {
const pinia = createPinia().use(somePlugin)
app.use(pinia)
setActivePinia(pinia)
})
```
## Testing Components
Install `@pinia/testing`:
```bash
npm i -D @pinia/testing
```
Use `createTestingPinia()`:
```ts
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { useSomeStore } from '@/stores/myStore'
const wrapper = mount(Counter, {
global: {
plugins: [createTestingPinia()],
},
})
const store = useSomeStore()
// Manipulate state directly
store.name = 'new name'
store.$patch({ name: 'new name' })
// Actions are stubbed by default
store.someAction()
expect(store.someAction).toHaveBeenCalledTimes(1)
```
## Initial State
Set initial state for tests:
```ts
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
initialState: {
counter: { n: 20 }, // Store name → initial state
},
}),
],
},
})
```
## Action Stubbing
### Execute Real Actions
```ts
createTestingPinia({ stubActions: false })
```
### Selective Stubbing
```ts
// Only stub specific actions
createTestingPinia({
stubActions: ['increment', 'reset'],
})
// Or use a function
createTestingPinia({
stubActions: (actionName, store) => {
if (actionName.startsWith('set')) return true
return false
},
})
```
### Mock Action Return Values
```ts
import type { Mock } from 'vitest'
// After getting store
store.someAction.mockResolvedValue('mocked value')
```
## Mocking Getters
Getters are writable in tests:
```ts
const pinia = createTestingPinia()
const counter = useCounterStore(pinia)
counter.double = 3 // Override computed value
// Reset to default behavior
counter.double = undefined
counter.double // Now computed normally
```
## Custom Spy Function
If not using Jest/Vitest with globals:
```ts
import { vi } from 'vitest'
createTestingPinia({
createSpy: vi.fn,
})
```
With Sinon:
```ts
import sinon from 'sinon'
createTestingPinia({
createSpy: sinon.spy,
})
```
## Pinia Plugins in Tests
Pass plugins to `createTestingPinia()`:
```ts
import { somePlugin } from '../src/stores/plugin'
createTestingPinia({
stubActions: false,
plugins: [somePlugin],
})
```
**Don't use** `testingPinia.use(MyPlugin)` - pass plugins in options.
## Type-Safe Mocked Store
```ts
import type { Mock } from 'vitest'
import type { Store, StoreDefinition } from 'pinia'
function mockedStore<TStoreDef extends () => unknown>(
useStore: TStoreDef
): TStoreDef extends StoreDefinition<infer Id, infer State, infer Getters, infer Actions>
? Store<Id, State, Record<string, never>, {
[K in keyof Actions]: Actions[K] extends (...args: any[]) => any
? Mock<Actions[K]>
: Actions[K]
}>
: ReturnType<TStoreDef> {
return useStore() as any
}
// Usage
const store = mockedStore(useSomeStore)
store.someAction.mockResolvedValue('value') // Typed!
```
## E2E Tests
No special handling needed - Pinia works normally.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/testing.html
-->
@@ -0,0 +1,389 @@
---
name: stores
description: Defining stores, state, getters, and actions in Pinia
---
# Pinia Stores
Stores are defined using `defineStore()` with a unique name. Each store has three core concepts: **state**, **getters**, and **actions**.
## Defining Stores
### Option Stores
Similar to Vue's Options API:
```ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo',
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
```
Think of `state` as `data`, `getters` as `computed`, and `actions` as `methods`.
### Setup Stores (Recommended)
Uses Composition API syntax - more flexible and powerful:
```ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Eduardo')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, name, doubleCount, increment }
})
```
In Setup Stores: `ref()` → state, `computed()` → getters, `function()` → actions.
**Important:** You must return all state properties for Pinia to track them.
### Using Stores
```vue
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// Access: store.count, store.doubleCount, store.increment()
</script>
```
### Destructuring with storeToRefs
```vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// ❌ Breaks reactivity
const { name, doubleCount } = store
// ✅ Preserves reactivity for state/getters
const { name, doubleCount } = storeToRefs(store)
// ✅ Actions can be destructured directly
const { increment } = store
</script>
```
---
## State
State is defined as a function returning the initial state.
### TypeScript
Type inference works automatically. For complex types:
```ts
interface UserInfo {
name: string
age: number
}
export const useUserStore = defineStore('user', {
state: () => ({
userList: [] as UserInfo[],
user: null as UserInfo | null,
}),
})
```
Or use an interface for the return type:
```ts
interface State {
userList: UserInfo[]
user: UserInfo | null
}
export const useUserStore = defineStore('user', {
state: (): State => ({
userList: [],
user: null,
}),
})
```
### Accessing and Modifying
```ts
const store = useStore()
store.count++
```
```vue
<input v-model="store.count" type="number" />
```
### Mutating with $patch
Apply multiple changes at once:
```ts
// Object syntax
store.$patch({
count: store.count + 1,
name: 'DIO',
})
// Function syntax (for complex mutations)
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
```
### Resetting State
Option Stores have built-in `$reset()`. For Setup Stores, implement your own:
```ts
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})
```
### Subscribing to State Changes
```ts
cartStore.$subscribe((mutation, state) => {
mutation.type // 'direct' | 'patch object' | 'patch function'
mutation.storeId // 'cart'
mutation.payload // patch object (only for 'patch object')
localStorage.setItem('cart', JSON.stringify(state))
})
// Options
cartStore.$subscribe(callback, { flush: 'sync' }) // Immediate
cartStore.$subscribe(callback, { detached: true }) // Keep after unmount
```
---
## Getters
Getters are computed values, equivalent to Vue's `computed()`.
### Basic Getters
```ts
getters: {
doubleCount: (state) => state.count * 2,
}
```
### Accessing Other Getters
Use `this` with explicit return type:
```ts
getters: {
doubleCount: (state) => state.count * 2,
doublePlusOne(): number {
return this.doubleCount + 1
},
},
```
### Getters with Arguments
Return a function (note: loses caching):
```ts
getters: {
getUserById: (state) => {
return (userId: string) => state.users.find((user) => user.id === userId)
},
},
```
Cache within parameterized getters:
```ts
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId: string) => activeUsers.find((user) => user.id === userId)
},
},
```
### Accessing Other Stores in Getters
```ts
import { useOtherStore } from './other-store'
getters: {
combined(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
```
---
## Actions
Actions are methods for business logic. Unlike getters, they can be asynchronous.
### Defining Actions
```ts
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
```
### Async Actions
```ts
actions: {
async registerUser(login: string, password: string) {
try {
this.userData = await api.post({ login, password })
} catch (error) {
return error
}
},
},
```
### Accessing Other Stores in Actions
```ts
import { useAuthStore } from './auth-store'
actions: {
async fetchUserPreferences() {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
}
},
},
```
**SSR:** Call all `useStore()` before any `await`:
```ts
async orderCart() {
// ✅ Call stores before await
const user = useUserStore()
await apiOrderCart(user.token, this.items)
// ❌ Don't call useStore() after await in SSR
}
```
### Subscribing to Actions
```ts
const unsubscribe = someStore.$onAction(
({ name, store, args, after, onError }) => {
const startTime = Date.now()
console.log(`Start "${name}" with params [${args.join(', ')}]`)
after((result) => {
console.log(`Finished "${name}" after ${Date.now() - startTime}ms`)
})
onError((error) => {
console.warn(`Failed "${name}": ${error}`)
})
}
)
unsubscribe() // Cleanup
```
Keep subscription after component unmount:
```ts
someStore.$onAction(callback, true)
```
---
## Options API Helpers
```ts
import { mapState, mapWritableState, mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
computed: {
// Readonly state/getters
...mapState(useCounterStore, ['count', 'doubleCount']),
// Writable state
...mapWritableState(useCounterStore, ['count']),
},
methods: {
...mapActions(useCounterStore, ['increment']),
},
}
```
---
## Accessing Global Providers in Setup Stores
```ts
import { inject } from 'vue'
import { useRoute } from 'vue-router'
import { defineStore } from 'pinia'
export const useSearchFilters = defineStore('search-filters', () => {
const route = useRoute()
const appProvided = inject('appProvided')
// Don't return these - access them directly in components
return { /* ... */ }
})
```
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/
- https://pinia.vuejs.org/core-concepts/state.html
- https://pinia.vuejs.org/core-concepts/getters.html
- https://pinia.vuejs.org/core-concepts/actions.html
-->
@@ -0,0 +1,114 @@
---
name: composables-in-stores
description: Using Vue composables within Pinia stores
---
# Composables in Stores
Pinia stores can leverage Vue composables for reusable stateful logic.
## Option Stores
Call composables inside the `state` property, but only those returning writable refs:
```ts
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
})
```
**Works:** Composables returning `ref()`:
- `useLocalStorage`
- `useAsyncState`
**Doesn't work in Option Stores:**
- Composables exposing functions
- Composables exposing readonly data
## Setup Stores
More flexible - can use almost any composable:
```ts
import { defineStore } from 'pinia'
import { useMediaControls } from '@vueuse/core'
import { ref } from 'vue'
export const useVideoPlayer = defineStore('video', () => {
const videoElement = ref<HTMLVideoElement>()
const src = ref('/data/video.mp4')
const { playing, volume, currentTime, togglePictureInPicture } =
useMediaControls(videoElement, { src })
function loadVideo(element: HTMLVideoElement, newSrc: string) {
videoElement.value = element
src.value = newSrc
}
return {
src,
playing,
volume,
currentTime,
loadVideo,
togglePictureInPicture,
}
})
```
**Note:** Don't return non-serializable DOM refs like `videoElement` - they're internal implementation details.
## SSR Considerations
### Option Stores with hydrate()
Define a `hydrate()` function to handle client-side hydration:
```ts
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
hydrate(state, initialState) {
// Ignore server state, read from browser
state.user = useLocalStorage('pinia/auth/login', 'bob')
},
})
```
### Setup Stores with skipHydrate()
Mark state that shouldn't hydrate from server:
```ts
import { defineStore, skipHydrate } from 'pinia'
import { useEyeDropper, useLocalStorage } from '@vueuse/core'
export const useColorStore = defineStore('colors', () => {
const { isSupported, open, sRGBHex } = useEyeDropper()
const lastColor = useLocalStorage('lastColor', sRGBHex)
return {
// Skip hydration for client-only state
lastColor: skipHydrate(lastColor),
open, // Function - no hydration needed
isSupported, // Boolean - not reactive
}
})
```
`skipHydrate()` only applies to state properties (refs), not functions or non-reactive values.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/composables.html
-->
@@ -0,0 +1,134 @@
---
name: composing-stores
description: Store-to-store communication and avoiding circular dependencies
---
# Composing Stores
Stores can use each other for shared state and logic.
## Rule: Avoid Circular Dependencies
Two stores cannot directly read each other's state during setup:
```ts
// ❌ Infinite loop
const useX = defineStore('x', () => {
const y = useY()
y.name // Don't read here!
return { name: ref('X') }
})
const useY = defineStore('y', () => {
const x = useX()
x.name // Don't read here!
return { name: ref('Y') }
})
```
**Solution:** Read in getters, computed, or actions:
```ts
const useX = defineStore('x', () => {
const y = useY()
// ✅ Read in computed/actions
function doSomething() {
const yName = y.name
}
return { name: ref('X'), doSomething }
})
```
## Setup Stores: Use Store at Top
```ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const user = useUserStore()
const list = ref([])
const summary = computed(() => {
return `Hi ${user.name}, you have ${list.value.length} items`
})
function purchase() {
return apiPurchase(user.id, list.value)
}
return { list, summary, purchase }
})
```
## Shared Getters
Call `useStore()` inside a getter:
```ts
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
getters: {
summary(state) {
const user = useUserStore()
return `Hi ${user.name}, you have ${state.list.length} items`
},
},
})
```
## Shared Actions
Call `useStore()` inside an action:
```ts
import { useUserStore } from './user'
import { apiOrderCart } from './api'
export const useCartStore = defineStore('cart', {
actions: {
async orderCart() {
const user = useUserStore()
try {
await apiOrderCart(user.token, this.items)
this.emptyCart()
} catch (err) {
displayError(err)
}
},
},
})
```
## SSR: Call Stores Before Await
In async actions, call all stores before any `await`:
```ts
actions: {
async orderCart() {
// ✅ All useStore() calls before await
const user = useUserStore()
const analytics = useAnalyticsStore()
try {
await apiOrderCart(user.token, this.items)
// ❌ Don't call useStore() after await (SSR issue)
// const otherStore = useOtherStore()
} catch (err) {
displayError(err)
}
},
}
```
This ensures the correct Pinia instance is used during SSR.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/composing-stores.html
-->
@@ -0,0 +1,203 @@
---
name: plugins
description: Extend stores with custom properties, methods, and behavior
---
# Plugins
Plugins extend all stores with custom properties, methods, or behavior.
## Basic Plugin
```ts
import { createPinia } from 'pinia'
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
pinia.use(SecretPiniaPlugin)
// In any store
const store = useStore()
store.secret // 'the cake is a lie'
```
## Plugin Context
Plugins receive a context object:
```ts
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
context.pinia // pinia instance
context.app // Vue app instance
context.store // store being augmented
context.options // store definition options
}
```
## Adding Properties
Return an object to add properties (tracked in devtools):
```ts
pinia.use(() => ({ hello: 'world' }))
```
Or set directly on store:
```ts
pinia.use(({ store }) => {
store.hello = 'world'
// For devtools visibility in dev mode
if (process.env.NODE_ENV === 'development') {
store._customProperties.add('hello')
}
})
```
## Adding State
Add to both `store` and `store.$state` for SSR/devtools:
```ts
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty('hasError')) {
const hasError = ref(false)
store.$state.hasError = hasError
}
store.hasError = toRef(store.$state, 'hasError')
})
```
## Adding External Properties
Wrap non-reactive objects with `markRaw()`:
```ts
import { markRaw } from 'vue'
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})
```
## Custom Store Options
Define custom options consumed by plugins:
```ts
// Store definition
defineStore('search', {
actions: {
searchContacts() { /* ... */ },
},
debounce: {
searchContacts: 300,
},
})
// Plugin reads custom option
import debounce from 'lodash/debounce'
pinia.use(({ options, store }) => {
if (options.debounce) {
return Object.keys(options.debounce).reduce((acc, action) => {
acc[action] = debounce(store[action], options.debounce[action])
return acc
}, {})
}
})
```
For Setup Stores, pass options as third argument:
```ts
defineStore(
'search',
() => { /* ... */ },
{
debounce: { searchContacts: 300 },
}
)
```
## TypeScript Augmentation
### Custom Properties
```ts
import 'pinia'
import type { Router } from 'vue-router'
declare module 'pinia' {
export interface PiniaCustomProperties {
router: Router
hello: string
}
}
```
### Custom State
```ts
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hasError: boolean
}
}
```
### Custom Options
```ts
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}
```
## Subscribe in Plugins
```ts
pinia.use(({ store }) => {
store.$subscribe(() => {
// React to state changes
})
store.$onAction(() => {
// React to actions
})
})
```
## Nuxt Plugin
Create a Nuxt plugin to add Pinia plugins:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`)
})
return { creationTime: new Date() }
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})
```
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/plugins.html
-->
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2025-present VoidZero Inc. & Contributors
Copyright (c) 2024 Kevin Deng (https://github.com/sxzz)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+77
View File
@@ -0,0 +1,77 @@
# tsdown Skills
Agent skills that help AI coding agents understand and work with [tsdown](https://tsdown.dev), the elegant library bundler.
## Installation
```bash
npx skills add rolldown/tsdown
```
This will install all tsdown skills (including the migration skill). To install only the tsdown skill:
```bash
npx skills add rolldown/tsdown --skill tsdown
```
## What's Included
The tsdown skill provides Claude Code with knowledge about:
- **Core Concepts** - What tsdown is, why use it, key features
- **Configuration** - Config file formats, options, multiple configs, workspace support
- **Build Options** - Entry points, output formats, type declarations, targets
- **Dependency Handling** - External/inline dependencies, auto-externalization
- **Output Enhancement** - Shims, CJS defaults, package exports
- **Framework Support** - React, Vue, Solid, Svelte integration
- **Advanced Features** - Plugins, hooks, programmatic API, Rolldown options
- **CLI Commands** - All CLI options and usage patterns
- **Migration** - Migrating from tsup to tsdown
## Usage
Once installed, Claude Code will automatically use tsdown knowledge when:
- Building TypeScript/JavaScript libraries
- Configuring bundlers for library projects
- Setting up type declaration generation
- Working with multi-format builds (ESM, CJS, IIFE, UMD)
- Migrating from tsup
- Building framework component libraries
### Example Prompts
```
Set up tsdown to build my TypeScript library with ESM and CJS formats
```
```
Configure tsdown to generate type declarations and bundle for browsers
```
```
Add React support to my tsdown config with Fast Refresh
```
```
Help me migrate from tsup to tsdown
```
```
Set up a monorepo build with tsdown workspace support
```
## Related Skills
- **[tsdown-migrate](https://github.com/rolldown/tsdown/tree/main/skills/tsdown-migrate)** - Dedicated skill for migrating from tsup to tsdown, with complete option mappings, config transformations, and troubleshooting guidance.
## Documentation
- [tsdown Documentation](https://tsdown.dev)
- [GitHub Repository](https://github.com/rolldown/tsdown)
- [Rolldown](https://rolldown.rs)
- [Migration Guide](https://tsdown.dev/guide/migrate-from-tsup)
## License
MIT
+416
View File
@@ -0,0 +1,416 @@
---
name: tsdown
description: Bundle TypeScript and JavaScript libraries with blazing-fast speed powered by Rolldown. Use when building libraries, generating type declarations, bundling for multiple formats, or migrating from tsup.
---
# tsdown - The Elegant Library Bundler
Blazing-fast bundler for TypeScript/JavaScript libraries powered by Rolldown and Oxc.
## Runtime Requirement
`tsdown` requires **Node.js 22.18.0 or higher to run** (build-time only). However, the bundled output can target much lower Node.js versions via the [`target`](references/option-target.md) option, so libraries built with tsdown are **not locked to Node.js 22+ at runtime**.
If your package needs to support Node.js 18 / 20:
- **Build with Node.js 22+ in CI** (e.g. set `target: 'node18'` or `target: 'node20'`).
- **Test the built output (or the packed tarball) on the lower Node.js versions** you intend to support — e.g. using a matrix job that runs the published package's tests on Node.js 18 / 20 / 22.
## When to Use
- Building TypeScript/JavaScript libraries for npm
- Generating TypeScript declaration files (.d.ts)
- Bundling for multiple formats (ESM, CJS, IIFE, UMD)
- Optimizing bundles with tree shaking and minification
- Migrating from tsup with minimal changes
- Building React, Vue, Solid, or Svelte component libraries
## Quick Start
```bash
# Install
pnpm add -D tsdown
# Basic usage
npx tsdown
# With config file
npx tsdown --config tsdown.config.ts
# Watch mode
npx tsdown --watch
# Migrate from tsup
npx tsdown-migrate
```
## Basic Configuration
```ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['./src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
## Core References
| Topic | Description | Reference |
|-------|-------------|-----------|
| Getting Started | Installation, first bundle, CLI basics | [guide-getting-started](references/guide-getting-started.md) |
| Configuration File | Config file formats, multiple configs, workspace | [option-config-file](references/option-config-file.md) |
| CLI Reference | All CLI commands and options | [reference-cli](references/reference-cli.md) |
| Migrate from tsup | Migration guide and compatibility notes | [guide-migrate-from-tsup](references/guide-migrate-from-tsup.md) |
| Plugins | Rolldown, Rollup, Unplugin support | [advanced-plugins](references/advanced-plugins.md) |
> For comprehensive migration assistance with complete option mappings, install the dedicated [`tsdown-migrate`](../tsdown-migrate/SKILL.md) skill: `npx skills add rolldown/tsdown --skill tsdown-migrate`
| Hooks | Lifecycle hooks for custom logic | [advanced-hooks](references/advanced-hooks.md) |
| Programmatic API | Build from Node.js scripts | [advanced-programmatic](references/advanced-programmatic.md) |
| Rolldown Options | Pass options directly to Rolldown | [advanced-rolldown-options](references/advanced-rolldown-options.md) |
| CI Environment | CI detection, `'ci-only'` / `'local-only'` values | [advanced-ci](references/advanced-ci.md) |
## Build Options
| Option | Usage | Reference |
|--------|-------|-----------|
| Entry points | `entry: ['src/*.ts', '!**/*.test.ts']` | [option-entry](references/option-entry.md) |
| Output formats | `format: ['esm', 'cjs', 'iife', 'umd']` | [option-output-format](references/option-output-format.md) |
| Output directory | `outDir: 'dist'`, `outExtensions` | [option-output-directory](references/option-output-directory.md) |
| Type declarations | `dts: true`, `dts: { sourcemap, compilerOptions, vue }` | [option-dts](references/option-dts.md) |
| Target environment | `target: 'es2020'`, `target: 'esnext'` | [option-target](references/option-target.md) |
| Platform | `platform: 'node'`, `platform: 'browser'` | [option-platform](references/option-platform.md) |
| Tree shaking | `treeshake: true`, custom options | [option-tree-shaking](references/option-tree-shaking.md) |
| Minification | `minify: true`, `minify: 'dce-only'` | [option-minification](references/option-minification.md) |
| Source maps | `sourcemap: true`, `'inline'`, `'hidden'` | [option-sourcemap](references/option-sourcemap.md) |
| Watch mode | `watch: true`, watch options | [option-watch-mode](references/option-watch-mode.md) |
| Cleaning | `clean: true`, clean patterns | [option-cleaning](references/option-cleaning.md) |
| Log level | `logLevel: 'silent'`, `failOnWarn: false` | [option-log-level](references/option-log-level.md) |
## Dependency Handling
| Feature | Usage | Reference |
|---------|-------|-----------|
| Never bundle | `deps: { neverBundle: ['react', /^@myorg\//] }` | [option-dependencies](references/option-dependencies.md) |
| Always bundle | `deps: { alwaysBundle: ['dep-to-bundle'] }` | [option-dependencies](references/option-dependencies.md) |
| Only bundle | `deps: { onlyBundle: ['cac', 'bumpp'] }` - Whitelist | [option-dependencies](references/option-dependencies.md) |
| Skip node_modules | `deps: { skipNodeModulesBundle: true }` | [option-dependencies](references/option-dependencies.md) |
| Auto external | Automatic dependency/peer/optional externalization | [option-dependencies](references/option-dependencies.md) |
## Output Enhancement
| Feature | Usage | Reference |
|---------|-------|-----------|
| Shims | `shims: true` - Add ESM/CJS compatibility | [option-shims](references/option-shims.md) |
| CJS default | `cjsDefault: true` (default) / `false` | [option-cjs-default](references/option-cjs-default.md) |
| Package exports | `exports: true` - Generate exports field | [option-package-exports](references/option-package-exports.md) |
| CSS handling | **[experimental]** `css: { ... }` — full pipeline with preprocessors, Lightning CSS, PostCSS, CSS modules, code splitting; requires `@tsdown/css` | [option-css](references/option-css.md) |
| CSS modules | `css: { modules: { localsConvention: 'camelCase' } }` — scoped class names for `.module.css` files | [option-css](references/option-css.md) |
| CSS inject | `css: { inject: true }` — preserve CSS imports in JS output | [option-css](references/option-css.md) |
| Unbundle mode | `unbundle: true` - Preserve directory structure | [option-unbundle](references/option-unbundle.md) |
| Root directory | `root: 'src'` - Control output directory mapping | [option-root](references/option-root.md) |
| Executable | **[experimental]** `exe: true` - Bundle as standalone executable, cross-platform via `@tsdown/exe` | [option-exe](references/option-exe.md) |
| Package validation | `publint: true`, `attw: true` - Validate package | [option-lint](references/option-lint.md) |
## Framework & Runtime Support
| Framework | Guide | Reference |
|-----------|-------|-----------|
| React | JSX transform, React Compiler | [recipe-react](references/recipe-react.md) |
| Vue | SFC support, JSX | [recipe-vue](references/recipe-vue.md) |
| Solid | SolidJS JSX transform | [recipe-solid](references/recipe-solid.md) |
| Svelte | Svelte component libraries (source distribution recommended) | [recipe-svelte](references/recipe-svelte.md) |
| WASM | WebAssembly modules via `rolldown-plugin-wasm` | [recipe-wasm](references/recipe-wasm.md) |
## Common Patterns
### Basic Library Bundle
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### Multiple Entry Points
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
utils: 'src/utils.ts',
cli: 'src/cli.ts',
},
format: ['esm', 'cjs'],
dts: true,
})
```
### Browser Library (IIFE/UMD)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['iife'],
globalName: 'MyLib',
platform: 'browser',
minify: true,
})
```
### React Component Library
```ts
export default defineConfig({
entry: ['src/index.tsx'],
format: ['esm', 'cjs'],
dts: true,
deps: {
neverBundle: ['react', 'react-dom'],
},
inputOptions: {
jsx: { runtime: 'automatic' },
},
})
```
### Preserve Directory Structure
```ts
export default defineConfig({
entry: ['src/**/*.ts', '!**/*.test.ts'],
unbundle: true, // Preserve file structure
format: ['esm'],
dts: true,
})
```
### CI-Aware Configuration
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
failOnWarn: 'ci-only', // opt-in: fail on warnings in CI
publint: 'ci-only',
attw: 'ci-only',
})
```
### WASM Support
```ts
import { wasm } from 'rolldown-plugin-wasm'
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
plugins: [wasm()],
})
```
### Library with CSS and Sass
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
target: 'chrome100',
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "src/styles/variables" as *;`,
},
},
},
})
```
### Standalone Executable
```ts
export default defineConfig({
entry: ['src/cli.ts'],
exe: true,
})
```
### Cross-Platform Executable (requires `@tsdown/exe`)
```ts
export default defineConfig({
entry: ['src/cli.ts'],
exe: {
targets: [
{ platform: 'linux', arch: 'x64', nodeVersion: '25.7.0' },
{ platform: 'darwin', arch: 'arm64', nodeVersion: '25.7.0' },
{ platform: 'win', arch: 'x64', nodeVersion: '25.7.0' },
],
},
})
```
### Advanced with Hooks
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
hooks: {
'build:before': async (context) => {
console.log('Building...')
},
'build:done': async (context) => {
console.log('Build complete!')
},
},
})
```
## Configuration Features
### Multiple Configs
Export an array for multiple build configurations:
```ts
export default defineConfig([
{
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
},
{
entry: ['src/cli.ts'],
format: ['esm'],
platform: 'node',
},
])
```
### Conditional Config
Use functions for dynamic configuration:
```ts
export default defineConfig((options) => {
const isDev = options.watch
return {
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
minify: !isDev,
sourcemap: isDev,
}
})
```
### Workspace/Monorepo
Use glob patterns to build multiple packages:
```ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
```
## CLI Quick Reference
```bash
# Basic commands
tsdown # Build once
tsdown --watch # Watch mode
tsdown --config custom.ts # Custom config
npx tsdown-migrate # Migrate from tsup
# Output options
tsdown --format esm,cjs # Multiple formats
tsdown -d lib # Custom output directory (--out-dir)
tsdown --minify # Enable minification
tsdown --dts # Generate declarations
tsdown --exe # Bundle as standalone executable
tsdown --unbundle # Bundleless mode
# Entry options
tsdown src/index.ts # Single entry
tsdown src/*.ts # Glob patterns
tsdown src/a.ts src/b.ts # Multiple entries
# Workspace / Monorepo
tsdown -W # Enable workspace mode
tsdown -W -F my-package # Filter specific package
tsdown --filter /^pkg-/ # Filter by regex
# Development
tsdown --watch # Watch mode
tsdown --sourcemap # Generate source maps
tsdown --clean # Clean output directory
tsdown --from-vite # Reuse Vite config
tsdown --tsconfig tsconfig.build.json # Custom tsconfig
```
## Best Practices
1. **Always generate type declarations** for TypeScript libraries:
```ts
{ dts: true }
```
2. **Externalize dependencies** to avoid bundling unnecessary code:
```ts
{ deps: { neverBundle: [/^react/, /^@myorg\//] } }
```
3. **Use tree shaking** for optimal bundle size:
```ts
{ treeshake: true }
```
4. **Enable minification** for production builds:
```ts
{ minify: true }
```
5. **Add shims** for better ESM/CJS compatibility:
```ts
{ shims: true } // Adds __dirname, __filename, etc.
```
6. **Auto-generate package.json exports**:
```ts
{ exports: true } // Creates proper exports field
```
7. **Use watch mode** during development:
```bash
tsdown --watch
```
8. **Preserve structure** for utilities with many files:
```ts
{ unbundle: true } // Keep directory structure
```
9. **Validate packages** in CI before publishing:
```ts
{ publint: 'ci-only', attw: 'ci-only' }
```
## Resources
- Documentation: https://tsdown.dev
- GitHub: https://github.com/rolldown/tsdown
- Rolldown: https://rolldown.rs
- Migration Guide: https://tsdown.dev/guide/migrate-from-tsup
+5
View File
@@ -0,0 +1,5 @@
# Sync Info
- **Source:** `vendor/tsdown/skills/tsdown`
- **Git SHA:** `f635a43b3c8b18569b47f3789c801f44a45c668a`
- **Synced:** 2026-06-22
+139
View File
@@ -0,0 +1,139 @@
# tsdown Skill References
This directory contains detailed reference documentation for the tsdown skill.
## Created Files (35 total)
### Core Guides (3)
-`guide-getting-started.md` - Installation, first bundle, CLI basics
-`guide-migrate-from-tsup.md` - Migration guide from tsup
-`guide-introduction.md` - Introduction and key features
### Configuration Options (20)
-`option-config-file.md` - Config file formats, loaders, workspace
-`option-entry.md` - Entry point configuration with globs
-`option-output-format.md` - Output formats (ESM, CJS, IIFE, UMD)
-`option-output-directory.md` - Output directory and extensions
-`option-dts.md` - TypeScript declaration generation
-`option-target.md` - Target environment (ES2020, ESNext, etc.)
-`option-platform.md` - Platform (node, browser, neutral)
-`option-dependencies.md` - External and inline dependencies
-`option-sourcemap.md` - Source map generation
-`option-minification.md` - Minification (`boolean | 'dce-only' | MinifyOptions`)
-`option-tree-shaking.md` - Tree shaking configuration
-`option-cleaning.md` - Output directory cleaning
-`option-watch-mode.md` - Watch mode configuration
-`option-shims.md` - ESM/CJS compatibility shims
-`option-package-exports.md` - Auto-generate package.json exports
-`option-css.md` - CSS handling (experimental, full pipeline: preprocessors, Lightning CSS, PostCSS, code splitting)
-`option-unbundle.md` - Preserve directory structure
-`option-cjs-default.md` - CommonJS default export handling
-`option-log-level.md` - Logging configuration
-`option-lint.md` - Package validation (publint & attw)
### Executable (1)
-`option-exe.md` - Standalone executable bundling (Node.js SEA)
### Advanced Topics (6)
-`advanced-plugins.md` - Rolldown, Rollup, Unplugin support
-`advanced-hooks.md` - Lifecycle hooks system
-`advanced-programmatic.md` - Node.js API usage
-`advanced-rolldown-options.md` - Pass options to Rolldown
-`advanced-ci.md` - CI environment detection and CI-aware options
### Advanced (continued)
-`advanced-benchmark.md` - Performance benchmarks
### Framework Recipes (5)
-`recipe-react.md` - React library setup with JSX
-`recipe-vue.md` - Vue library setup with SFC
-`recipe-solid.md` - Solid.js library setup
-`recipe-svelte.md` - Svelte component libraries
-`recipe-wasm.md` - WASM module support
### Reference (1)
-`reference-cli.md` - Complete CLI command reference
## Coverage Status
**Created:** 35 files (100% complete)
## Current Skill Features
The tsdown skill now includes comprehensive coverage of:
### ✅ Core Functionality
- Getting started and installation
- Entry points and glob patterns
- Output formats (ESM, CJS, IIFE, UMD)
- TypeScript declarations
- Configuration file setup
- CLI reference
### ✅ Build Options
- Target environment configuration
- Platform selection
- Dependency management
- Source maps
- Minification
- Tree shaking
- Output cleaning
- Watch mode
### ✅ Advanced Features
- Plugins (Rolldown, Rollup, Unplugin)
- Lifecycle hooks
- ESM/CJS shims
- Package exports generation
- Package validation (publint, attw)
- Programmatic API (Node.js)
- Output directory customization
- CSS handling and modules
- Unbundle mode
- CI environment detection and CI-aware options
### ✅ Framework & Runtime Support
- React with JSX/TSX
- React Compiler integration
- Vue with SFC support
- Vue type generation (vue-tsc)
- WASM module bundling (rolldown-plugin-wasm)
### ✅ Migration
- Complete migration guide from tsup
- Compatibility notes
## Usage
The skill is now ready for use with comprehensive coverage of core features. Additional files can be added incrementally as needed.
## File Naming Convention
Files are prefixed by category:
- `guide-*` - Getting started guides and tutorials
- `option-*` - Configuration options
- `advanced-*` - Advanced topics (plugins, hooks, programmatic API)
- `recipe-*` - Framework-specific recipes
- `reference-*` - CLI and API reference
## Creating New Reference Files
When creating new reference files:
1. **Read source documentation** from `/docs` directory
2. **Simplify for AI consumption** - concise, actionable content
3. **Include code examples** - practical, copy-paste ready
4. **Add cross-references** - link to related options
5. **Follow naming convention** - use appropriate prefix
6. **Keep it focused** - one topic per file
## Updating Existing Files
When documentation changes:
1. Check git diff: `git diff <sha>..HEAD -- docs/`
2. Update affected reference files
3. Update SKILL.md if needed
4. Update GENERATION.md with new SHA
See `skills/GENERATION.md` for detailed update instructions.
@@ -0,0 +1,8 @@
# Benchmark
tsdown delivers exceptional performance:
- **~2x faster** than tsup for standard builds
- **Up to 8x faster** for TypeScript declaration generation
For detailed comparisons, see [bundler-benchmark](https://gugustinette.github.io/bundler-benchmark/).
@@ -0,0 +1,89 @@
# CI Environment Support
Automatically detect CI environments and toggle features based on local vs CI builds.
## Overview
tsdown detects CI from the `CI` environment variable. CI mode is enabled when `process.env.CI` is set to a value other than `0` or `false` (case-insensitive).
## CI-Aware Values
Several options accept CI-aware string values:
| Value | Behavior |
|-------|----------|
| `true` | Always enabled |
| `false` | Always disabled |
| `'ci-only'` | Enabled only in CI, disabled locally |
| `'local-only'` | Enabled only locally, disabled in CI |
## Supported Options
These options accept CI-aware values:
- `dts` - TypeScript declaration file generation
- `publint` - Package lint validation
- `attw` - "Are the types wrong" validation
- `report` - Bundle size reporting
- `exports` - Auto-generate `package.json` exports
- `unused` - Unused dependency check
- `devtools` - DevTools integration
- `failOnWarn` - Fail on warnings (defaults to `false`)
## Usage
### String Form
```ts
export default defineConfig({
dts: 'local-only', // Skip DTS in CI for faster builds
publint: 'ci-only', // Only run publint in CI
failOnWarn: 'ci-only', // Fail on warnings in CI only (opt-in)
})
```
### Object Form
When an option takes a configuration object, set `enabled` to a CI-aware value:
```ts
export default defineConfig({
publint: {
enabled: 'ci-only',
level: 'error',
},
attw: {
enabled: 'ci-only',
profile: 'node16',
},
})
```
### Config Function
The config function receives a `ci` boolean in its context:
```ts
export default defineConfig((_, { ci }) => ({
minify: ci,
sourcemap: !ci,
}))
```
## Typical CI Configuration
```ts
export default defineConfig({
entry: 'src/index.ts',
format: ['esm', 'cjs'],
dts: true,
failOnWarn: 'ci-only',
publint: 'ci-only',
attw: 'ci-only',
})
```
## Related Options
- [Package Validation](option-lint.md) - publint and attw configuration
- [Log Level](option-log-level.md) - `failOnWarn` option details
@@ -0,0 +1,363 @@
# Lifecycle Hooks
Extend the build process with lifecycle hooks.
## Overview
Hooks provide a way to inject custom logic at specific stages of the build lifecycle. Inspired by [unbuild](https://github.com/unjs/unbuild).
**Recommendation:** Use [plugins](advanced-plugins.md) for most extensions. Use hooks for simple custom tasks or Rolldown plugin injection.
## Usage Patterns
### Object Syntax
```ts
export default defineConfig({
entry: ['src/index.ts'],
hooks: {
'build:prepare': async (context) => {
console.log('Build starting...')
},
'build:done': async (context) => {
console.log('Build complete!')
},
},
})
```
### Function Syntax
```ts
export default defineConfig({
entry: ['src/index.ts'],
hooks(hooks) {
hooks.hook('build:prepare', () => {
console.log('Preparing build...')
})
hooks.hook('build:before', (context) => {
console.log(`Building format: ${context.format}`)
})
},
})
```
## Available Hooks
### `build:prepare`
Called before the build process starts.
**When:** Once per build session
**Context:**
```ts
{
options: ResolvedConfig,
hooks: Hookable
}
```
**Use cases:**
- Setup tasks
- Validation
- Environment preparation
**Example:**
```ts
hooks: {
'build:prepare': async (context) => {
console.log('Starting build for:', context.options.entry)
await cleanOldFiles()
},
}
```
### `build:before`
Called before each Rolldown build.
**When:** Once per format (ESM, CJS, etc.)
**Context:**
```ts
{
options: ResolvedConfig,
buildOptions: BuildOptions,
hooks: Hookable
}
```
**Use cases:**
- Modify build options per format
- Inject plugins dynamically
- Format-specific setup
**Example:**
```ts
hooks: {
'build:before': async (context) => {
console.log(`Building ${context.buildOptions.format} format...`)
// Add format-specific plugin
if (context.buildOptions.format === 'iife') {
context.buildOptions.plugins.push(browserPlugin())
}
},
}
```
### `build:done`
Called after the build completes.
**When:** Once per build session
**Context:**
```ts
{
options: ResolvedConfig,
chunks: RolldownChunk[],
hooks: Hookable
}
```
**Use cases:**
- Post-processing
- Asset copying
- Notifications
- Deployment
**Example:**
```ts
hooks: {
'build:done': async (context) => {
console.log(`Built ${context.chunks.length} chunks`)
// Copy additional files
await copyAssets()
// Send notification
notifyBuildComplete()
},
}
```
## Common Patterns
### Build Notifications
```ts
export default defineConfig({
hooks: {
'build:prepare': () => {
console.log('🚀 Starting build...')
},
'build:done': (context) => {
const size = context.chunks.reduce((sum, c) => sum + c.code.length, 0)
console.log(`✅ Build complete! Total size: ${size} bytes`)
},
},
})
```
### Conditional Plugin Injection
```ts
export default defineConfig({
hooks(hooks) {
hooks.hook('build:before', (context) => {
// Add minification only for production
if (process.env.NODE_ENV === 'production') {
context.buildOptions.plugins.push(minifyPlugin())
}
})
},
})
```
### Custom File Copy
```ts
import { copyFile } from 'fs/promises'
export default defineConfig({
hooks: {
'build:done': async (context) => {
// Copy README to dist
await copyFile('README.md', `${context.options.outDir}/README.md`)
},
},
})
```
### Build Metrics
```ts
export default defineConfig({
hooks: {
'build:prepare': (context) => {
context.startTime = Date.now()
},
'build:done': (context) => {
const duration = Date.now() - context.startTime
console.log(`Build took ${duration}ms`)
// Log chunk sizes
context.chunks.forEach((chunk) => {
console.log(`${chunk.fileName}: ${chunk.code.length} bytes`)
})
},
},
})
```
### Format-Specific Logic
```ts
export default defineConfig({
format: ['esm', 'cjs', 'iife'],
hooks: {
'build:before': (context) => {
const format = context.buildOptions.format
if (format === 'iife') {
// Browser-specific setup
context.buildOptions.globalName = 'MyLib'
} else if (format === 'cjs') {
// Node-specific setup
context.buildOptions.platform = 'node'
}
},
},
})
```
### Deployment Hook
```ts
export default defineConfig({
hooks: {
'build:done': async (context) => {
if (process.env.DEPLOY === 'true') {
console.log('Deploying to CDN...')
await deployToCDN(context.options.outDir)
}
},
},
})
```
## Advanced Usage
### Multiple Hooks
```ts
export default defineConfig({
hooks(hooks) {
// Register multiple hooks
hooks.hook('build:prepare', setupEnvironment)
hooks.hook('build:prepare', validateConfig)
hooks.hook('build:before', injectPlugins)
hooks.hook('build:before', logFormat)
hooks.hook('build:done', generateManifest)
hooks.hook('build:done', notifyComplete)
},
})
```
### Async Hooks
```ts
export default defineConfig({
hooks: {
'build:prepare': async (context) => {
await fetchRemoteConfig()
await initializeDatabase()
},
'build:done': async (context) => {
await uploadToS3(context.chunks)
await invalidateCDN()
},
},
})
```
### Error Handling
```ts
export default defineConfig({
hooks: {
'build:done': async (context) => {
try {
await riskyOperation()
} catch (error) {
console.error('Hook failed:', error)
// Don't throw - allow build to complete
}
},
},
})
```
## Hookable API
tsdown uses [hookable](https://github.com/unjs/hookable) for hooks. Additional methods:
```ts
export default defineConfig({
hooks(hooks) {
// Register hook
hooks.hook('build:done', handler)
// Register hook once
hooks.hookOnce('build:prepare', handler)
// Remove hook
hooks.removeHook('build:done', handler)
// Clear all hooks for event
hooks.removeHooks('build:done')
// Call hooks manually
await hooks.callHook('build:done', context)
},
})
```
## Tips
1. **Use plugins** for most extensions
2. **Hooks for simple tasks** like notifications or file copying
3. **Async hooks supported** for I/O operations
4. **Don't throw errors** unless you want to fail the build
5. **Context is mutable** in `build:before` for advanced use cases
6. **Multiple hooks allowed** for the same event
## Troubleshooting
### Hook Not Called
- Verify hook name is correct
- Check hook is registered in config
- Ensure async hooks are awaited
### Build Fails in Hook
- Add try/catch for error handling
- Don't throw unless intentional
- Log errors for debugging
### Context Undefined
- Check which hook you're using
- Verify context properties available for that hook
## Related
- [Plugins](advanced-plugins.md) - Plugin system
- [Rolldown Options](advanced-rolldown-options.md) - Build options
- [Watch Mode](option-watch-mode.md) - Development workflow
@@ -0,0 +1,381 @@
# Plugins
Extend tsdown with plugins from multiple ecosystems.
## Overview
tsdown, built on Rolldown, supports plugins from multiple ecosystems to extend and customize the bundling process.
## Supported Ecosystems
### 1. Rolldown Plugins
Native plugins designed for Rolldown:
```ts
import RolldownPlugin from 'rolldown-plugin-something'
export default defineConfig({
plugins: [RolldownPlugin()],
})
```
**Compatibility:** ✅ Full support
### 2. Unplugin
Universal plugins that work across bundlers:
```ts
import UnpluginPlugin from 'unplugin-something'
export default defineConfig({
plugins: [UnpluginPlugin.rolldown()],
})
```
**Compatibility:** ✅ Most unplugin-* plugins work
**Examples:**
- `unplugin-vue-components`
- `unplugin-auto-import`
- `unplugin-icons`
### 3. Rollup Plugins
Most Rollup plugins work with tsdown:
```ts
import RollupPlugin from '@rollup/plugin-something'
export default defineConfig({
plugins: [RollupPlugin()],
})
```
**Compatibility:** ✅ High compatibility
**Type Issues:** May cause TypeScript errors - use type casting:
```ts
import RollupPlugin from 'rollup-plugin-something'
export default defineConfig({
plugins: [
// @ts-expect-error Rollup plugin type mismatch
RollupPlugin(),
// Or cast to any
RollupPlugin() as any,
],
})
```
### 4. Vite Plugins
Some Vite plugins may work:
```ts
import VitePlugin from 'vite-plugin-something'
export default defineConfig({
plugins: [
// @ts-expect-error Vite plugin type mismatch
VitePlugin(),
],
})
```
**Compatibility:** ⚠️ Limited - only if not using Vite-specific APIs
**Note:** Improved support planned for future releases.
## Usage
### Basic Plugin Usage
```ts
import { defineConfig } from 'tsdown'
import SomePlugin from 'some-plugin'
export default defineConfig({
entry: ['src/index.ts'],
plugins: [SomePlugin()],
})
```
### Multiple Plugins
```ts
import PluginA from 'plugin-a'
import PluginB from 'plugin-b'
import PluginC from 'plugin-c'
export default defineConfig({
entry: ['src/index.ts'],
plugins: [
PluginA(),
PluginB({ option: true }),
PluginC(),
],
})
```
### Conditional Plugins
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
plugins: [
SomePlugin(),
options.watch && DevPlugin(),
!options.watch && ProdPlugin(),
].filter(Boolean),
}))
```
## Common Plugin Patterns
### JSON Import
```ts
import json from '@rollup/plugin-json'
export default defineConfig({
plugins: [json()],
})
```
### Node Resolve
```ts
import { nodeResolve } from '@rollup/plugin-node-resolve'
export default defineConfig({
plugins: [nodeResolve()],
})
```
### CommonJS
```ts
import commonjs from '@rollup/plugin-commonjs'
export default defineConfig({
plugins: [commonjs()],
})
```
### Replace
```ts
import replace from '@rollup/plugin-replace'
export default defineConfig({
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
__VERSION__: JSON.stringify('1.0.0'),
}),
],
})
```
### Auto Import
```ts
import AutoImport from 'unplugin-auto-import/rolldown'
export default defineConfig({
plugins: [
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts',
}),
],
})
```
### Vue Components
```ts
import Components from 'unplugin-vue-components/rolldown'
export default defineConfig({
plugins: [
Components({
dts: 'src/components.d.ts',
}),
],
})
```
## Framework-Specific Plugins
### React
```ts
import react from '@vitejs/plugin-react'
export default defineConfig({
entry: ['src/index.tsx'],
plugins: [
// @ts-expect-error Vite plugin
react(),
],
})
```
### Vue
```ts
import vue from '@vitejs/plugin-vue'
export default defineConfig({
entry: ['src/index.ts'],
plugins: [
// @ts-expect-error Vite plugin
vue(),
],
})
```
### Solid
```ts
import solid from 'vite-plugin-solid'
export default defineConfig({
entry: ['src/index.tsx'],
plugins: [
// @ts-expect-error Vite plugin
solid(),
],
})
```
### Svelte
```ts
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
entry: ['src/index.ts'],
plugins: [
// @ts-expect-error Vite plugin
svelte(),
],
})
```
## Writing Custom Plugins
Follow Rolldown's plugin development guide:
### Basic Plugin Structure
```ts
import type { Plugin } from 'rolldown'
function myPlugin(): Plugin {
return {
name: 'my-plugin',
// Transform hook
transform(code, id) {
if (id.endsWith('.custom')) {
return {
code: transformCode(code),
map: null,
}
}
},
// Other hooks...
}
}
```
### Using Custom Plugin
```ts
import { myPlugin } from './my-plugin'
export default defineConfig({
plugins: [myPlugin()],
})
```
## Plugin Configuration
### Plugin-Specific Options
Refer to each plugin's documentation for configuration options.
### Plugin Order
Plugins run in the order they're defined:
```ts
export default defineConfig({
plugins: [
PluginA(), // Runs first
PluginB(), // Runs second
PluginC(), // Runs last
],
})
```
## Troubleshooting
### Type Errors with Rollup/Vite Plugins
Use type casting:
```ts
plugins: [
// Option 1: @ts-expect-error
// @ts-expect-error Plugin type mismatch
SomePlugin(),
// Option 2: as any
SomePlugin() as any,
]
```
### Plugin Not Working
1. **Check compatibility** - Verify plugin supports your bundler
2. **Read documentation** - Follow plugin's setup instructions
3. **Check plugin order** - Some plugins depend on execution order
4. **Enable debug mode** - Use `--debug` flag
### Vite Plugin Fails
Vite plugins may rely on Vite-specific APIs:
1. **Find Rollup equivalent** - Look for Rollup version of plugin
2. **Use Unplugin version** - Check for `unplugin-*` alternative
3. **Wait for support** - Vite plugin support improving
## Resources
- [Rolldown Plugin Development](https://rolldown.rs/apis/plugin-api)
- [Unplugin Documentation](https://unplugin.unjs.io/)
- [Rollup Plugins](https://github.com/rollup/plugins)
- [Vite Plugins](https://vitejs.dev/plugins/)
## Tips
1. **Prefer Rolldown plugins** for best compatibility
2. **Use Unplugin** for cross-bundler support
3. **Cast types** for Rollup/Vite plugins
4. **Test thoroughly** when using cross-ecosystem plugins
5. **Check plugin docs** for specific configuration
6. **Write custom plugins** for unique needs
## Related
- [Hooks](advanced-hooks.md) - Lifecycle hooks
- [Rolldown Options](advanced-rolldown-options.md) - Advanced Rolldown config
- [React Recipe](recipe-react.md) - React setup with plugins
- [Vue Recipe](recipe-vue.md) - Vue setup with plugins
@@ -0,0 +1,378 @@
# Programmatic Usage
Use tsdown from JavaScript/TypeScript code.
## Overview
tsdown can be imported and used programmatically in your Node.js scripts, custom build tools, or automation workflows.
## Basic Usage
### Simple Build
```ts
import { build } from 'tsdown'
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
```
### With Options
```ts
import { build } from 'tsdown'
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
outDir: 'dist',
dts: true,
minify: true,
sourcemap: true,
clean: true,
})
```
## API Reference
### build()
Main function to run a build.
```ts
import { build } from 'tsdown'
await build(options)
```
**Parameters:**
- `options` - Build configuration object (same as config file)
**Returns:**
- `Promise<void>` - Resolves when build completes
**Throws:**
- Build errors if compilation fails
## Configuration Object
All config file options are available:
```ts
import { build, defineConfig } from 'tsdown'
const config = defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
minify: true,
sourcemap: true,
deps: {
neverBundle: ['react', 'react-dom'],
},
plugins: [/* plugins */],
hooks: {
'build:done': async () => {
console.log('Build complete!')
},
},
})
await build(config)
```
See [Config Reference](option-config-file.md) for all options.
## Common Patterns
### Custom Build Script
```ts
// scripts/build.ts
import { build } from 'tsdown'
async function main() {
console.log('Building library...')
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
console.log('Build complete!')
}
main().catch(console.error)
```
Run with:
```bash
tsx scripts/build.ts
```
### Multiple Builds
```ts
import { build } from 'tsdown'
// Build main library
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
outDir: 'dist',
dts: true,
})
// Build CLI tool
await build({
entry: ['src/cli.ts'],
format: ['esm'],
outDir: 'dist/bin',
platform: 'node',
shims: true,
})
```
### Conditional Build
```ts
import { build } from 'tsdown'
const isDev = process.env.NODE_ENV === 'development'
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
minify: !isDev,
sourcemap: isDev,
clean: !isDev,
})
```
### With Error Handling
```ts
import { build } from 'tsdown'
try {
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
console.log('✅ Build successful')
} catch (error) {
console.error('❌ Build failed:', error)
process.exit(1)
}
```
### Automated Workflow
```ts
import { build } from 'tsdown'
import { execSync } from 'child_process'
async function release() {
// Clean
console.log('Cleaning...')
execSync('rm -rf dist')
// Build
console.log('Building...')
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
minify: true,
})
// Test
console.log('Testing...')
execSync('npm test')
// Publish
console.log('Publishing...')
execSync('npm publish')
}
release().catch(console.error)
```
### Build with Post-Processing
```ts
import { build } from 'tsdown'
import { copyFileSync } from 'fs'
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
hooks: {
'build:done': async () => {
// Copy additional files
copyFileSync('README.md', 'dist/README.md')
copyFileSync('LICENSE', 'dist/LICENSE')
console.log('Copied additional files')
},
},
})
```
## Watch Mode
Unfortunately, watch mode is not directly exposed in the programmatic API. Use the CLI for watch mode:
```ts
// Use CLI for watch mode
import { spawn } from 'child_process'
spawn('tsdown', ['--watch'], {
stdio: 'inherit',
shell: true,
})
```
## Integration Examples
### With Task Runner
```ts
// gulpfile.js
import { build } from 'tsdown'
import gulp from 'gulp'
gulp.task('build', async () => {
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
})
gulp.task('watch', () => {
return gulp.watch('src/**/*.ts', gulp.series('build'))
})
```
### With Custom CLI
```ts
// scripts/cli.ts
import { build } from 'tsdown'
import { Command } from 'commander'
const program = new Command()
program
.command('build')
.option('--prod', 'Production build')
.action(async (options) => {
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
minify: options.prod,
sourcemap: !options.prod,
})
})
program.parse()
```
### With CI/CD
```ts
// .github/scripts/build.ts
import { build } from 'tsdown'
const isCI = process.env.CI === 'true'
await build({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
minify: isCI,
clean: true,
})
// Upload to artifact storage
if (isCI) {
// Upload dist/ to S3, etc.
}
```
## TypeScript Support
```ts
// scripts/build.ts
import { build, type UserConfig } from 'tsdown'
const config: UserConfig = {
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
}
await build(config)
```
## Tips
1. **Use TypeScript** for type safety
2. **Handle errors** properly
3. **Use hooks** for custom logic
4. **Log progress** for visibility
5. **Use CLI for watch** mode
6. **Exit on error** in scripts
## Troubleshooting
### Import Errors
Ensure tsdown is installed:
```bash
pnpm add -D tsdown
```
### Type Errors
Import types:
```ts
import type { UserConfig } from 'tsdown'
```
### Build Fails Silently
Add error handling:
```ts
try {
await build(config)
} catch (error) {
console.error(error)
process.exit(1)
}
```
### Options Not Working
Check spelling and types:
```ts
// ✅ Correct
{ format: ['esm', 'cjs'] }
// ❌ Wrong
{ formats: ['esm', 'cjs'] }
```
## Related
- [Config File](option-config-file.md) - Configuration options
- [Hooks](advanced-hooks.md) - Lifecycle hooks
- [CLI](reference-cli.md) - Command-line interface
- [Plugins](advanced-plugins.md) - Plugin system
@@ -0,0 +1,117 @@
# Customizing Rolldown Options
Pass options directly to the underlying Rolldown bundler.
## Overview
tsdown uses [Rolldown](https://rolldown.rs) as its core bundling engine. You can override Rolldown's input and output options directly for fine-grained control.
**Warning:** You should be familiar with Rolldown's behavior before overriding options. Refer to the [Rolldown Config Options](https://rolldown.rs/options/input) documentation.
## Input Options
### Using an Object
```ts
export default defineConfig({
inputOptions: {
cwd: './custom-directory',
},
})
```
### Using a Function
Dynamically modify options based on the output format:
```ts
export default defineConfig({
inputOptions(inputOptions, format) {
inputOptions.cwd = './custom-directory'
return inputOptions
},
})
```
## Output Options
### Using an Object
```ts
export default defineConfig({
outputOptions: {
legalComments: 'inline',
},
})
```
### Using a Function
```ts
export default defineConfig({
outputOptions(outputOptions, format) {
if (format === 'esm') {
outputOptions.legalComments = 'inline'
}
return outputOptions
},
})
```
## Common Use Cases
### Preserve Legal Comments
```ts
export default defineConfig({
entry: ['src/index.ts'],
outputOptions: {
legalComments: 'inline',
},
})
```
### Custom Working Directory
```ts
export default defineConfig({
entry: ['src/index.ts'],
inputOptions: {
cwd: './packages/my-lib',
},
})
```
### Format-Specific Options
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
outputOptions(outputOptions, format) {
if (format === 'esm') {
outputOptions.legalComments = 'inline'
}
return outputOptions
},
})
```
## When to Use
- When tsdown doesn't expose a specific Rolldown option
- For format-specific Rolldown customizations
- For advanced bundling scenarios
## Tips
1. **Read Rolldown docs** before overriding options
2. **Use functions** for format-specific customization
3. **Test thoroughly** when overriding defaults
4. **Prefer tsdown options** when available (e.g., use `minify` instead of setting it via `outputOptions`)
## Related
- [Plugins](advanced-plugins.md) - Plugin system
- [Hooks](advanced-hooks.md) - Lifecycle hooks
- [Config File](option-config-file.md) - Configuration options
@@ -0,0 +1,183 @@
# Getting Started
Quick guide to installing and using tsdown for the first time.
## Installation
Install tsdown as a development dependency:
```bash
pnpm add -D tsdown
# Optionally install TypeScript if not using isolatedDeclarations
pnpm add -D typescript
```
**Requirements:**
- Node.js 22.18.0 or higher **to run tsdown** (build-time only)
- Experimental support for Deno and Bun
> [!NOTE]
> The Node.js 22.18+ requirement only applies to the environment that runs `tsdown` itself. The **bundled output** can target much lower Node.js versions via the [`target`](./option-target.md) option, so libraries built with tsdown are not locked to Node.js 22+ at runtime.
>
> If your package needs to support Node.js 18 / 20, the recommended workflow is to **build with Node.js 22+ in CI**, then **test the built output (or the packed tarball) against the lower Node.js versions** you intend to support.
## Quick Start Templates
Use `create-tsdown` CLI for instant setup:
```bash
pnpm create tsdown@latest
```
Provides templates for:
- Pure TypeScript libraries
- React component libraries
- Vue component libraries
- Ready-to-use configurations
## First Bundle
### 1. Create Source Files
```ts
// src/index.ts
import { hello } from './hello.ts'
hello()
// src/hello.ts
export function hello() {
console.log('Hello tsdown!')
}
```
### 2. Create Config File
```ts
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['./src/index.ts'],
})
```
### 3. Run Build
```bash
./node_modules/.bin/tsdown
```
Output: `dist/index.mjs`
### 4. Test Output
```bash
node dist/index.mjs
# Output: Hello tsdown!
```
## Add to npm Scripts
```json
{
"scripts": {
"build": "tsdown"
}
}
```
Run with:
```bash
pnpm build
```
## CLI Commands
```bash
# Check version
tsdown --version
# View help
tsdown --help
# Build with watch mode
tsdown --watch
# Build with specific format
tsdown --format esm,cjs
# Generate type declarations
tsdown --dts
```
## Basic Configurations
### TypeScript Library (ESM + CJS)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### Browser Library (IIFE)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['iife'],
globalName: 'MyLib',
platform: 'browser',
minify: true,
})
```
### Multiple Entry Points
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
utils: 'src/utils.ts',
cli: 'src/cli.ts',
},
format: ['esm', 'cjs'],
dts: true,
})
```
## Using Plugins
Add Rolldown, Rollup, or Unplugin plugins:
```ts
import SomePlugin from 'some-plugin'
export default defineConfig({
entry: ['src/index.ts'],
plugins: [SomePlugin()],
})
```
## Watch Mode
Enable automatic rebuilds on file changes:
```bash
tsdown --watch
# or
tsdown -w
```
## Next Steps
- Configure [entry points](option-entry.md) with glob patterns
- Set up [multiple output formats](option-output-format.md)
- Enable [type declaration generation](option-dts.md)
- Explore [plugins](advanced-plugins.md) for extended functionality
- Read [migration guide](guide-migrate-from-tsup.md) if coming from tsup
@@ -0,0 +1,42 @@
# Introduction
**tsdown** is _The Elegant Library Bundler_ — a fast, simple bundler for TypeScript and JavaScript libraries powered by Rolldown (Rust-based).
## Why tsdown?
Built on [Rolldown](https://rolldown.rs), tsdown provides a complete out-of-the-box solution for library authors:
- **Simplified Configuration**: Sensible defaults for library development, minimal boilerplate
- **Library-Specific Features**: Auto TypeScript declarations, multiple output formats, package validation
- **Future-Ready**: Official Rolldown project, foundation for Rolldown Vite's Library Mode
## Plugin Ecosystem
Supports the full Rolldown plugin ecosystem plus most Rollup plugins. See [Plugins](advanced-plugins.md).
## What Can It Bundle?
- **TypeScript/JavaScript**: `.ts`, `.js` with modern syntax
- **TypeScript Declarations**: Auto-generate `.d.ts` files
- **Multiple Formats**: `esm`, `cjs`, `iife`, `umd`
- **Assets**: `.json`, `.wasm`, CSS files
- Built-in tree shaking, minification, and source maps
## Key Differences from Rolldown
tsdown wraps Rolldown with library-specific features:
- Auto-external `dependencies`, `peerDependencies`, and `optionalDependencies` from `package.json`
- DTS generation
- `package.json` exports field generation
- Watch mode with keyboard shortcuts
- CSS preprocessing pipeline
- Executable bundling (SEA)
## Prior Arts
Inspired by: Rollup, esbuild, tsup, unbuild. Powered by Rolldown.
## Related
- [Getting Started](guide-getting-started.md) - Installation and first build
- [Migrate from tsup](guide-migrate-from-tsup.md) - Migration guide
@@ -0,0 +1,199 @@
# Migrate from tsup
Migration guide for switching from tsup to tsdown.
## Overview
tsdown is built on Rolldown (Rust-based) vs tsup's esbuild, providing faster and more powerful bundling while maintaining compatibility.
## Automatic Migration
### Single Package
```bash
npx tsdown-migrate
```
### Monorepo
```bash
# Using glob patterns
npx tsdown-migrate packages/*
# Multiple directories
npx tsdown-migrate packages/foo packages/bar
```
### Migration Options
- `[...dirs]` - Directories to migrate (supports globs)
- `--dry-run` or `-d` - Preview changes without modifying files
**Important:** Commit your changes before running migration.
## Key Differences
### Default Values
| Option | tsup | tsdown |
|--------|------|--------|
| `format` | `['cjs']` | `['esm']` |
| `clean` | `false` | `true` |
| `dts` | `false` | Auto-enabled if `types`/`typings` in package.json |
| `target` | Manual | Auto-read from `engines.node` in package.json |
### Option Renames
| tsup | tsdown |
|------|--------|
| `outExtension` | `outExtensions` |
### Output Filename Differences
For IIFE builds, `tsdown` emits `[name].iife.js`; `tsup` commonly emitted `[name].global.js`. `outExtensions` customizes extensions or suffixes, but it does not remove `.iife` or `.umd`. Use `outputOptions.entryFileNames: '[name].global.js'` to preserve old IIFE filenames.
### New Features in tsdown
#### Node Protocol Control
```ts
export default defineConfig({
nodeProtocol: true, // Add node: prefix (fs → node:fs)
nodeProtocol: 'strip', // Remove node: prefix (node:fs → fs)
nodeProtocol: false, // Keep as-is (default)
})
```
#### Better Workspace Support
```ts
export default defineConfig({
workspace: 'packages/*', // Build all packages
})
```
## Migration Checklist
1. **Backup your code** - Commit all changes
2. **Run migration tool** - `npx tsdown-migrate`
3. **Review changes** - Check modified config files
4. **Update scripts** - Change `tsup` to `tsdown` in package.json
5. **Test build** - Run `pnpm build` to verify
6. **Adjust config** - Fine-tune based on your needs
## Common Migration Patterns
### Basic Library
**Before (tsup):**
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
})
```
**After (tsdown):**
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'], // ESM now default
dts: true,
clean: true, // Now enabled by default
})
```
### With Custom Target
**Before (tsup):**
```ts
export default defineConfig({
entry: ['src/index.ts'],
target: 'es2020',
})
```
**After (tsdown):**
```ts
export default defineConfig({
entry: ['src/index.ts'],
// target auto-reads from package.json engines.node
// Or override explicitly:
target: 'es2020',
})
```
### CLI Scripts
**Before (package.json):**
```json
{
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
}
}
```
**After (package.json):**
```json
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch"
}
}
```
## Feature Compatibility
### Supported tsup Features
Most tsup features are supported:
- ✅ Multiple entry points
- ✅ Multiple formats (ESM, CJS, IIFE, UMD)
- ✅ TypeScript declarations
- ✅ Source maps
- ✅ Minification
- ✅ Watch mode
- ✅ External dependencies
- ✅ Tree shaking
- ✅ Shims
- ✅ Plugins (Rollup compatible)
### Missing Features
Some tsup features are not yet available. Check [GitHub issues](https://github.com/rolldown/tsdown/issues) for status and request features.
## Troubleshooting
### Build Fails After Migration
1. **Check Node.js version** - Requires Node.js 22.18.0+ to run tsdown itself. The bundled output can still target lower Node.js versions via `target`; if you need to support Node.js 18 / 20, build with Node.js 22+ in CI and test the produced output (or packed tarball) on the lower versions.
2. **Install TypeScript** - Required for DTS generation
3. **Review config changes** - Ensure format and options are correct
4. **Check dependencies** - Verify all dependencies are installed
### Different Output
- **Format order** - tsdown defaults to ESM first
- **Clean behavior** - tsdown cleans outDir by default
- **Target** - tsdown auto-detects from package.json
### Performance Issues
tsdown should be faster than tsup. If not:
1. Enable `isolatedDeclarations` for faster DTS generation
2. Check for large dependencies being bundled
3. Use `skipNodeModulesBundle` if needed
## Getting Help
- [GitHub Issues](https://github.com/rolldown/tsdown/issues) - Report bugs or request features
- [Documentation](https://tsdown.dev) - Full documentation
- [Migration Tool](https://github.com/rolldown/tsdown/tree/main/packages/migrate) - Source code
## Acknowledgements
tsdown is heavily inspired by tsup and incorporates parts of its codebase. Thanks to [@egoist](https://github.com/egoist) and the tsup community.
@@ -0,0 +1,98 @@
# CJS Default Export
Control how default exports are handled in CommonJS output.
## Overview
The `cjsDefault` option improves compatibility when generating CommonJS modules. When enabled (default), modules with only a single default export use `module.exports = ...` instead of `exports.default = ...`.
## Type
```ts
cjsDefault?: boolean // default: true
```
## Basic Usage
### Enabled (Default)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs'],
cjsDefault: true, // default behavior
})
```
### Disabled
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs'],
cjsDefault: false,
})
```
## How It Works
### With `cjsDefault: true` (Default)
When your module has **only a single default export**, tsdown transforms:
**Source:**
```ts
// src/index.ts
export default function greet() {
console.log('Hello, world!')
}
```
**Generated CJS:**
```js
// dist/index.cjs
function greet() {
console.log('Hello, world!')
}
module.exports = greet
```
**Generated Declaration:**
```ts
// dist/index.d.cts
declare function greet(): void
export = greet
```
This allows consumers to use `const greet = require('your-module')` directly.
### With `cjsDefault: false`
The default export stays as `exports.default`:
```js
// dist/index.cjs
function greet() {
console.log('Hello, world!')
}
exports.default = greet
```
Consumers need `require('your-module').default`.
## When to Disable
- When your module has both default and named exports
- When you need consistent `exports.default` behavior
- When consumers always use ESM imports
## Tips
1. **Leave enabled** for most libraries (default `true`)
2. **Disable** if you have both default and named exports and need consistent behavior
3. **Test CJS consumers** to verify compatibility
## Related Options
- [Output Format](option-output-format.md) - Module formats
- [Shims](option-shims.md) - ESM/CJS compatibility
@@ -0,0 +1,275 @@
# Output Directory Cleaning
Control how the output directory is cleaned before builds.
## Overview
By default, tsdown **cleans the output directory** before each build to remove stale files from previous builds.
## Basic Usage
### CLI
```bash
# Clean enabled (default)
tsdown
# Disable cleaning
tsdown --no-clean
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
clean: true, // Default
})
```
## Behavior
### With Cleaning (Default)
Before each build:
1. All files in `outDir` are removed
2. Fresh build starts with empty directory
3. Only current build outputs remain
**Benefits:**
- No stale files
- Predictable output
- Clean slate each build
### Without Cleaning
Build outputs are added to existing files:
```ts
export default defineConfig({
clean: false,
})
```
**Use when:**
- Multiple builds to same directory
- Incremental builds
- Preserving other files
- Watch mode (faster rebuilds)
## Common Patterns
### Production Build
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
clean: true, // Ensure clean output
minify: true,
})
```
### Development Mode
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
clean: !options.watch, // Don't clean in watch mode
sourcemap: options.watch,
}))
```
### Multiple Builds
```ts
export default defineConfig([
{
entry: ['src/index.ts'],
outDir: 'dist',
clean: true, // Clean once
},
{
entry: ['src/cli.ts'],
outDir: 'dist',
clean: false, // Don't clean, add to same dir
},
])
```
### Monorepo Package
```ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
clean: true, // Clean each package's dist
})
```
### Preserve Static Files
```ts
export default defineConfig({
entry: ['src/index.ts'],
clean: false, // Keep manually added files
outDir: 'dist',
})
// Manually copy files first
// Then run tsdown --no-clean
```
## Clean Patterns
### Selective Cleaning
```ts
import { rmSync } from 'fs'
export default defineConfig({
clean: false, // Disable auto clean
hooks: {
'build:prepare': () => {
// Custom cleaning logic
rmSync('dist/*.js', { force: true })
// Keep other files
},
},
})
```
### Clean Specific Directories
```ts
export default defineConfig({
clean: false,
hooks: {
'build:prepare': async () => {
const { rm } = await import('fs/promises')
// Only clean specific subdirectories
await rm('dist/esm', { recursive: true, force: true })
await rm('dist/cjs', { recursive: true, force: true })
// Keep dist/types
},
},
})
```
## Watch Mode Behavior
In watch mode, cleaning behavior is important:
### Clean on First Build Only
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
watch: options.watch,
clean: !options.watch, // Only clean initial build
}))
```
**Result:**
- First build: Clean
- Subsequent rebuilds: Incremental
### Always Clean
```ts
export default defineConfig({
watch: true,
clean: true, // Clean every rebuild
})
```
**Trade-off:** Slower rebuilds, but always fresh output.
## Tips
1. **Leave enabled** for production builds
2. **Disable in watch mode** for faster rebuilds
3. **Use multiple configs** carefully with cleaning
4. **Custom clean logic** via hooks if needed
5. **Be cautious** - cleaning removes ALL files in outDir
6. **Test cleaning** - ensure no important files are lost
## Troubleshooting
### Important Files Deleted
- Don't put non-build files in outDir
- Use separate directory for static files
- Disable cleaning and manage manually
### Stale Files in Output
- Enable cleaning: `clean: true`
- Or manually remove before build
### Slow Rebuilds in Watch
- Disable cleaning in watch mode
- Use incremental builds
## CLI Examples
```bash
# Default (clean enabled)
tsdown
# Disable cleaning
tsdown --no-clean
# Watch mode without cleaning
tsdown --watch --no-clean
# Multiple formats with cleaning
tsdown --format esm,cjs --clean
```
## Examples
### Safe Production Build
```bash
# Clean before build
rm -rf dist
tsdown --clean
```
### Incremental Development
```ts
export default defineConfig({
entry: ['src/index.ts'],
watch: true,
clean: false, // Faster rebuilds
sourcemap: true,
})
```
### Multi-Stage Build
```ts
// Stage 1: Clean and build main
export default defineConfig([
{
entry: ['src/index.ts'],
outDir: 'dist',
clean: true,
},
{
entry: ['src/utils.ts'],
outDir: 'dist',
clean: false, // Add to same directory
},
])
```
## Related Options
- [Output Directory](option-output-directory.md) - Configure outDir
- [Watch Mode](option-watch-mode.md) - Development workflow
- [Hooks](advanced-hooks.md) - Custom clean logic
- [Entry](option-entry.md) - Entry points
@@ -0,0 +1,291 @@
# Configuration File
Centralize and manage build settings with a configuration file.
## Overview
tsdown searches for config files automatically in the current directory and parent directories.
## Supported File Names
tsdown looks for these files (in order):
- `tsdown.config.ts`
- `tsdown.config.mts`
- `tsdown.config.cts`
- `tsdown.config.js`
- `tsdown.config.mjs`
- `tsdown.config.cjs`
- `tsdown.config.json`
- `tsdown.config`
- `package.json` (in `tsdown` field)
## Basic Configuration
### TypeScript Config
```ts
// tsdown.config.ts
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
})
```
### JavaScript Config
```js
// tsdown.config.js
export default {
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
}
```
### JSON Config
```json
// tsdown.config.json
{
"entry": ["src/index.ts"],
"format": ["esm", "cjs"],
"dts": true
}
```
### Package.json Config
```json
// package.json
{
"name": "my-library",
"tsdown": {
"entry": ["src/index.ts"],
"format": ["esm", "cjs"],
"dts": true
}
}
```
## Multiple Configurations
Build multiple outputs with different settings:
```ts
export default defineConfig([
{
entry: 'src/index.ts',
format: ['esm', 'cjs'],
platform: 'node',
dts: true,
},
{
entry: 'src/browser.ts',
format: ['iife'],
platform: 'browser',
globalName: 'MyLib',
minify: true,
},
])
```
Each configuration runs as a separate build.
## Dynamic Configuration
Use a function for conditional config:
```ts
export default defineConfig((options) => {
const isDev = options.watch
return {
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
minify: !isDev,
sourcemap: isDev,
clean: !isDev,
}
})
```
Available options:
- `watch` - Whether watch mode is enabled
- Other CLI flags passed to config
## Config Loaders
Control how TypeScript config files are loaded:
### Auto Loader (Default)
Uses native TypeScript support if available, otherwise falls back to `unrun`:
```bash
tsdown # Uses auto loader
```
### Native Loader
Uses runtime's native TypeScript support (Node.js 22.18.0+, Bun, Deno):
```bash
tsdown --config-loader native
```
### tsx Loader
Uses [tsx](https://tsx.is/) library for loading via its tsImport API. Note: `tsx` is an optional peer dependency — install it manually first.
```bash
pnpm add -D tsx
tsdown --config-loader tsx
```
### Unrun Loader
Uses [unrun](https://gugustinette.github.io/unrun/) library for loading. Note: `unrun` is an optional peer dependency — install it manually first.
```bash
pnpm add -D unrun
tsdown --config-loader unrun
```
**Tip:** Use `tsx` or `unrun` loader if you need to load TypeScript configs without file extensions in Node.js.
## Custom Config Path
Specify a custom config file location:
```bash
tsdown --config ./configs/build.config.ts
# or
tsdown -c custom-config.ts
```
## Disable Config File
Ignore config files and use CLI options only:
```bash
tsdown --no-config src/index.ts --format esm
```
## Extend Vite/Vitest Config (Experimental)
Reuse existing Vite or Vitest configurations:
```bash
# Extend vite.config.*
tsdown --from-vite
# Extend vitest.config.*
tsdown --from-vite vitest
```
**Note:** Only specific options like `resolve` and `plugins` are reused. Test thoroughly as this feature is experimental.
## Workspace / Monorepo
Build multiple packages with a single config:
```ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
```
Each package directory matching the glob pattern will be built with the same configuration.
## Common Patterns
### Library with Multiple Builds
```ts
export default defineConfig([
// Node.js build
{
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
platform: 'node',
dts: true,
},
// Browser build
{
entry: ['src/browser.ts'],
format: ['iife'],
platform: 'browser',
globalName: 'MyLib',
},
])
```
### Development vs Production
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
minify: !options.watch,
sourcemap: options.watch ? true : false,
clean: !options.watch,
}))
```
### Monorepo Root Config
```ts
// Root tsdown.config.ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
// Shared config for all packages
})
```
### Per-Package Override
```ts
// packages/special/tsdown.config.ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'], // Override: only ESM
platform: 'browser', // Override: browser only
})
```
## Config Precedence
When multiple configs exist:
1. CLI options (highest priority)
2. Config file specified with `--config`
3. Auto-discovered config files
4. Package.json `tsdown` field
5. Default values
## Tips
1. **Use TypeScript config** for type checking and autocomplete
2. **Use defineConfig** helper for better DX
3. **Export arrays** for multiple build configurations
4. **Use functions** for dynamic/conditional configs
5. **Keep configs simple** - prefer convention over configuration
6. **Use workspace** for monorepo builds
7. **Test experimental features** thoroughly before production use
## Related Options
- [Entry](option-entry.md) - Configure entry points
- [Output Format](option-output-format.md) - Output formats
- [Watch Mode](option-watch-mode.md) - Watch mode configuration
@@ -0,0 +1,301 @@
# CSS Support
**Status: Experimental — API and behavior may change.**
Configure CSS handling including preprocessors, syntax lowering, minification, and code splitting.
## Getting Started
All CSS support in `tsdown` is provided by the `@tsdown/css` package. Install it to enable CSS handling:
```bash
npm install -D @tsdown/css
```
When `@tsdown/css` is installed, CSS processing is automatically enabled. Without it, encountering CSS files will result in an error.
## CSS Import
Import `.css` files from TypeScript/JavaScript — CSS is extracted into separate `.css` assets:
```ts
// src/index.ts
import './style.css'
export function greet() { return 'Hello' }
```
Output: `index.mjs` + `index.css`
### `@import` Inlining
CSS `@import` statements are resolved and inlined automatically. No separate output files produced.
### Inline CSS (`?inline`)
Append `?inline` to return processed CSS as a JS string instead of emitting a `.css` file:
```ts
import './style.css' // → .css file
import css from './theme.css?inline' // → JS string
```
Works with preprocessors too (`./foo.scss?inline`). Goes through full pipeline (preprocessors, @import inlining, lowering, minification). Tree-shakeable (`moduleSideEffects: false`).
## CSS Pre-processors
Built-in support for Sass, Less, and Stylus. Install the preprocessor:
```bash
# Sass (either one)
npm install -D sass-embedded # recommended, faster
npm install -D sass
# Less
npm install -D less
# Stylus
npm install -D stylus
```
Then import directly:
```ts
import './style.scss'
import './theme.less'
import './global.styl'
```
### Preprocessor Options
```ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `$brand-color: #ff7e17;`,
},
less: {
math: 'always',
},
stylus: {
define: { '$brand-color': '#ff7e17' },
},
},
},
})
```
### `additionalData`
Inject code at the beginning of every preprocessor file:
```ts
// String form
scss: {
additionalData: `@use "src/styles/variables" as *;`,
}
// Function form
scss: {
additionalData: (source, filename) => {
if (filename.includes('theme')) return source
return `@use "src/styles/variables" as *;\n${source}`
},
}
```
## CSS Minification
```ts
export default defineConfig({
css: {
minify: true,
},
})
```
Powered by Lightning CSS.
## CSS Target
Override the top-level `target` specifically for CSS:
```ts
export default defineConfig({
target: 'node18',
css: {
target: 'chrome90', // CSS-specific target
},
})
```
Set `css.target: false` to disable CSS syntax lowering entirely.
## CSS Transformer
`css.transformer` controls mutually exclusive CSS processing paths:
- `'lightningcss'` (default): `@import` via Lightning CSS `bundleAsync()`, no PostCSS.
- `'postcss'`: `@import` via `postcss-import`, PostCSS plugins applied, Lightning CSS for final transform only.
```ts
export default defineConfig({
css: {
transformer: 'postcss',
},
})
```
### PostCSS Options
```ts
export default defineConfig({
css: {
transformer: 'postcss',
postcss: {
plugins: [require('autoprefixer')],
},
// Or: postcss: './config' — path to search for postcss.config.js
},
})
```
Auto-detects PostCSS config from project root when `transformer` is `'postcss'` and `css.postcss` is omitted.
## Lightning CSS (Syntax Lowering)
Install `lightningcss` to enable CSS syntax lowering based on your `target`:
```bash
npm install -D lightningcss
```
When `target` is set (e.g., `target: 'chrome108'`), modern CSS features are automatically downleveled:
```css
/* Input */
.foo { & .bar { color: red } }
/* Output (chrome108) */
.foo .bar { color: red }
```
### Custom Lightning CSS Options
```ts
import { Features } from 'lightningcss'
export default defineConfig({
css: {
lightningcss: {
targets: { chrome: 100 << 16 },
include: Features.Nesting,
},
},
})
```
`css.lightningcss.targets` takes precedence over both `target` and `css.target` for CSS.
## CSS Modules
Files with `.module.css` (and `.module.scss`, `.module.less`, etc.) are treated as CSS modules — class names are scoped and exported as JS:
```ts
import styles from './app.module.css'
console.log(styles.title) // "scoped_title_hash"
```
### Configuration
```ts
export default defineConfig({
css: {
modules: {
scopeBehaviour: 'local', // 'local' (default) | 'global'
generateScopedName: '[hash]_[local]', // Lightning CSS pattern string
localsConvention: 'camelCase', // 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
},
},
})
```
Set `css.modules: false` to disable. Function-form `generateScopedName` requires `transformer: 'postcss'`.
### Optional Dependencies (PostCSS path)
```bash
npm install -D postcss postcss-modules
```
## Code Splitting
### Merged (Default)
All CSS merged into a single file (default: `style.css`).
```ts
export default defineConfig({
css: {
fileName: 'my-library.css', // Custom name (default: 'style.css')
},
})
```
### Per-Chunk Splitting
```ts
export default defineConfig({
css: {
splitting: true, // Each JS chunk gets a corresponding .css file
},
})
```
## Preserving CSS Imports (`css.inject`)
When enabled, JS output preserves `import` statements pointing to emitted CSS files. Consumers auto-import CSS alongside JS:
```ts
export default defineConfig({
css: {
inject: true,
},
})
```
## PostCSS Optional Peer Dependencies
When using `transformer: 'postcss'`, install these as needed:
| Package | Purpose | Required When |
|---------|---------|---------------|
| `postcss` | Core PostCSS engine | Always (with `transformer: 'postcss'`) |
| `postcss-import` | Resolve/inline `@import` | CSS uses `@import` |
| `postcss-modules` | CSS modules (scoped classes) | Using `.module.css` files |
```bash
npm install -D postcss postcss-import postcss-modules
```
All declared as optional peer dependencies of `@tsdown/css`.
## Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `css.transformer` | `'postcss' \| 'lightningcss'` | `'lightningcss'` | CSS processing pipeline |
| `css.splitting` | `boolean` | `false` | Per-chunk CSS splitting |
| `css.fileName` | `string` | `'style.css'` | Merged CSS file name |
| `css.minify` | `boolean` | `false` | CSS minification |
| `css.modules` | `object \| false` | `{}` | CSS modules config, or `false` to disable |
| `css.inject` | `boolean` | `false` | Preserve CSS imports in JS output |
| `css.target` | `string \| string[] \| false` | _from `target`_ | CSS-specific lowering target |
| `css.postcss` | `string \| object` | — | PostCSS config path or inline options |
| `css.preprocessorOptions` | `object` | — | Preprocessor options |
| `css.lightningcss` | `object` | — | Lightning CSS options |
## Related
- [Target](option-target.md) - Configure syntax lowering targets
- [Output Format](option-output-format.md) - Module output formats
@@ -0,0 +1,385 @@
# Dependencies
Control how dependencies are bundled or externalized.
## Overview
tsdown intelligently handles dependencies to keep your library lightweight while ensuring all necessary code is included.
## Default Behavior
### Auto-Externalized
These are **NOT bundled** by default:
- **`dependencies`** - Installed automatically with your package
- **`peerDependencies`** - User must install manually
- **`optionalDependencies`** - May or may not be installed depending on platform/config
### Conditionally Bundled
These are **bundled ONLY if imported**:
- **`devDependencies`** - Only if actually used in source code
- **Phantom dependencies** - In node_modules but not in package.json
## Configuration Options
All dependency options are grouped under the `deps` field:
```ts
export default defineConfig({
deps: {
neverBundle: ['react', /^@myorg\//],
alwaysBundle: ['some-package'],
onlyBundle: ['cac', 'bumpp'],
skipNodeModulesBundle: true,
},
})
```
### `deps.neverBundle`
Mark dependencies as external (not bundled):
```ts
export default defineConfig({
entry: ['src/index.ts'],
deps: {
neverBundle: [
'react', // Single package
'react-dom',
/^@myorg\//, // Regex pattern (all @myorg/* packages)
/^lodash/, // All lodash packages
],
},
})
```
### `deps.alwaysBundle`
Force dependencies to be bundled:
```ts
export default defineConfig({
entry: ['src/index.ts'],
deps: {
alwaysBundle: [
'some-package', // Bundle this even if in dependencies
'vendor-lib',
],
},
})
```
### `deps.onlyBundle`
Whitelist of dependencies allowed to be bundled from node_modules. Throws an error if any unlisted dependency is bundled:
```ts
export default defineConfig({
entry: ['src/index.ts'],
deps: {
onlyBundle: [
'cac', // Allow bundling cac
'bumpp', // Allow bundling bumpp
/^my-utils/, // Regex patterns supported
],
},
})
```
**Behavior:**
- **Array** (`['cac', /^my-/]`): Only matching dependencies can be bundled. Error for others.
- **`false`**: Suppress all warnings about bundled dependencies.
- **Not set** (default): Warns if any node_modules dependencies are bundled.
**Note:** Include all sub-dependencies in the list, not just top-level imports.
### `deps.skipNodeModulesBundle`
Skip bundling ALL node_modules:
```ts
export default defineConfig({
entry: ['src/index.ts'],
deps: {
skipNodeModulesBundle: true,
},
})
```
**Result:** No dependencies from node_modules are bundled.
**Note:** Cannot be used together with `alwaysBundle`.
## Common Patterns
### React Component Library
```ts
export default defineConfig({
entry: ['src/index.tsx'],
format: ['esm', 'cjs'],
deps: {
neverBundle: [
'react',
'react-dom',
/^react\//, // react/jsx-runtime, etc.
],
},
dts: true,
})
```
### Utility Library with Shared Deps
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
deps: {
alwaysBundle: ['lodash-es'],
},
dts: true,
})
```
### Monorepo Package
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
deps: {
neverBundle: [
/^@mycompany\//, // Don't bundle other workspace packages
],
},
dts: true,
})
```
### CLI Tool (Bundle Everything)
```ts
export default defineConfig({
entry: ['src/cli.ts'],
format: ['esm'],
platform: 'node',
deps: {
alwaysBundle: [/.*/],
},
shims: true,
})
```
### Library with Specific Externals
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
deps: {
neverBundle: [
'vue',
'@vue/runtime-core',
'@vue/reactivity',
],
},
dts: true,
})
```
## Declaration Files
Dependency handling for `.d.ts` files follows the same rules as JavaScript.
### Complex Type Resolution
Use TypeScript resolver for complex third-party types:
```ts
export default defineConfig({
entry: ['src/index.ts'],
dts: {
resolver: 'tsc', // Use TypeScript resolver instead of Oxc
},
})
```
**When to use `tsc` resolver:**
- Types in `@types/*` packages with non-standard naming (e.g., `@types/babel__generator`)
- Complex type dependencies
- Issues with default Oxc resolver
**Trade-off:** `tsc` is slower but more compatible.
## CLI Usage
### Never Bundle
```bash
tsdown --deps.never-bundle react --deps.never-bundle react-dom
tsdown --deps.never-bundle '/^@myorg\/.*/'
```
### Skip Node Modules
```bash
tsdown --deps.skip-node-modules-bundle
```
## Migration from Deprecated Options
| Deprecated Option | New Option |
|---|---|
| `external` | `deps.neverBundle` |
| `noExternal` | `deps.alwaysBundle` |
| `inlineOnly` | `deps.onlyBundle` |
| `deps.onlyAllowBundle` | `deps.onlyBundle` |
| `skipNodeModulesBundle` | `deps.skipNodeModulesBundle` |
## Examples by Use Case
### Framework Component
```ts
// Don't bundle framework
export default defineConfig({
deps: {
neverBundle: ['vue', 'react', 'solid-js', 'svelte'],
},
})
```
### Standalone App
```ts
// Bundle everything
export default defineConfig({
deps: {
alwaysBundle: [/.*/],
},
})
```
### Shared Library
```ts
// Bundle only specific utils
export default defineConfig({
deps: {
neverBundle: [/.*/], // External by default
alwaysBundle: ['tiny-utils'], // Except this one
},
})
```
### Monorepo Package
```ts
// External workspace packages, bundle utilities
export default defineConfig({
deps: {
neverBundle: [
/^@workspace\//, // Other workspace packages
'react',
'react-dom',
],
alwaysBundle: [
'lodash-es', // Bundle utility libraries
],
},
})
```
## Troubleshooting
### Dependency Bundled Unexpectedly
Check if it's in `devDependencies` and imported. Move to `dependencies`:
```json
{
"dependencies": {
"should-be-external": "^1.0.0"
}
}
```
Or explicitly externalize:
```ts
export default defineConfig({
deps: {
neverBundle: ['should-be-external'],
},
})
```
### Missing Dependency at Runtime
Ensure it's in `dependencies`, `peerDependencies`, or `optionalDependencies`:
```json
{
"dependencies": {
"needed-package": "^1.0.0"
}
}
```
Or bundle it:
```ts
export default defineConfig({
deps: {
alwaysBundle: ['needed-package'],
},
})
```
### Type Resolution Errors
Use TypeScript resolver for complex types:
```ts
export default defineConfig({
dts: {
resolver: 'tsc',
},
})
```
## Summary
**Default behavior:**
- `dependencies`, `peerDependencies`, & `optionalDependencies` → External
- `devDependencies` & phantom deps → Bundled if imported
**Override (under `deps`):**
- `neverBundle` → Force external
- `alwaysBundle` → Force bundled
- `onlyBundle` → Whitelist bundled deps
- `skipNodeModulesBundle` → Skip all node_modules
**Declaration files:**
- Same bundling logic as JavaScript
- Use `resolver: 'tsc'` for complex types
## Tips
1. **Keep dependencies external** for libraries
2. **Bundle everything** for standalone CLIs
3. **Use regex patterns** for namespaced packages
4. **Check bundle size** to verify external/bundled split
5. **Test with fresh install** to catch missing dependencies
6. **Use tsc resolver** only when needed (slower)
## Related Options
- [External](option-dependencies.md) - This page
- [Platform](option-platform.md) - Runtime environment
- [Output Format](option-output-format.md) - Module formats
- [DTS](option-dts.md) - Type declarations
@@ -0,0 +1,251 @@
# TypeScript Declaration Files
Generate `.d.ts` type declaration files for your library.
## Overview
tsdown uses [rolldown-plugin-dts](https://github.com/sxzz/rolldown-plugin-dts) to generate and bundle TypeScript declaration files.
**Requirements:**
- TypeScript must be installed in your project
## Enabling DTS Generation
### Auto-Enabled
DTS generation is **automatically enabled** if `package.json` contains:
- `types` field, or
- `typings` field
### Manual Enable
#### CLI
```bash
tsdown --dts
```
#### Config File
```ts
export default defineConfig({
dts: true,
})
```
## Performance
### With `isolatedDeclarations` (Recommended)
**Extremely fast** - uses oxc-transform for generation.
```json
// tsconfig.json
{
"compilerOptions": {
"isolatedDeclarations": true
}
}
```
### Without `isolatedDeclarations`
Falls back to TypeScript compiler. Reliable but slower.
## Declaration Maps
Map `.d.ts` files back to original `.ts` sources (useful for monorepos).
### Enable in tsconfig.json
```json
{
"compilerOptions": {
"declarationMap": true
}
}
```
### Enable in tsdown Config
```ts
export default defineConfig({
dts: {
sourcemap: true,
},
})
```
## Advanced Options
### Custom Compiler Options
Override TypeScript compiler options:
```ts
export default defineConfig({
dts: {
compilerOptions: {
removeComments: false,
},
},
})
```
## Build Process
- **ESM format**: `.js` and `.d.ts` files generated in same build
- **CJS format**: Separate build process for `.d.ts` files
## Common Patterns
### Basic Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
```
Output:
- `dist/index.mjs`
- `dist/index.cjs`
- `dist/index.d.ts`
### Multiple Entry Points
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
utils: 'src/utils.ts',
},
format: ['esm', 'cjs'],
dts: true,
})
```
Output:
- `dist/index.mjs`, `dist/index.cjs`, `dist/index.d.ts`
- `dist/utils.mjs`, `dist/utils.cjs`, `dist/utils.d.ts`
### With Monorepo Support
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: {
sourcemap: true, // Enable declaration maps
},
})
```
### Fast Build (Isolated Declarations)
```json
// tsconfig.json
{
"compilerOptions": {
"isolatedDeclarations": true,
"declaration": true,
"declarationMap": true
}
}
```
```ts
// tsdown.config.ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true, // Will use fast oxc-transform
})
```
## Troubleshooting
### Missing Types
Ensure TypeScript is installed:
```bash
pnpm add -D typescript
```
### Slow Generation
Enable `isolatedDeclarations` in `tsconfig.json` for faster builds.
### Declaration Errors
Check that all exports have explicit types (required for `isolatedDeclarations`).
### Report Issues
For DTS-specific issues, report to [rolldown-plugin-dts](https://github.com/sxzz/rolldown-plugin-dts/issues).
### Vue Support
Enable Vue component type generation (requires `vue-tsc`):
```ts
export default defineConfig({
dts: {
vue: true,
},
})
```
### Oxc Transform
Control Oxc usage for declaration generation:
```ts
export default defineConfig({
dts: {
oxc: true, // Use oxc-transform (fast, requires isolatedDeclarations)
},
})
```
### Custom TSConfig
Specify a different tsconfig for DTS generation:
```ts
export default defineConfig({
dts: {
tsconfig: './tsconfig.build.json',
},
})
```
## Available DTS Options
| Option | Type | Description |
|--------|------|-------------|
| `sourcemap` | `boolean` | Generate declaration source maps |
| `compilerOptions` | `object` | Override TypeScript compiler options |
| `vue` | `boolean` | Enable Vue type generation (requires vue-tsc) |
| `oxc` | `boolean` | Use oxc-transform for fast generation |
| `tsconfig` | `string` | Path to tsconfig file |
| `resolver` | `'oxc' \| 'tsc'` | Module resolver: `'oxc'` (default, fast) or `'tsc'` (more compatible) |
| `cjsDefault` | `boolean` | CJS default export handling |
| `sideEffects` | `boolean` | Preserve side effects in declarations |
## Tips
1. **Always enable DTS** for TypeScript libraries
2. **Use isolatedDeclarations** for fast builds
3. **Enable declaration maps** in monorepos
4. **Ensure explicit types** for all exports
5. **Install TypeScript** as dev dependency
## Related Options
- [Entry](option-entry.md) - Configure entry points
- [Output Format](option-output-format.md) - Multiple output formats
- [Target](option-target.md) - JavaScript version
@@ -0,0 +1,211 @@
# Entry Points
Configure which files to bundle as entry points.
## Overview
Entry points are the starting files for the bundling process. Each entry point generates a separate bundle.
## Usage Patterns
### CLI
```bash
# Single entry
tsdown src/index.ts
# Multiple entries
tsdown src/index.ts src/cli.ts
# Glob patterns
tsdown 'src/*.ts'
```
### Config File
#### Single Entry
```ts
export default defineConfig({
entry: 'src/index.ts',
})
```
#### Multiple Entries (Array)
```ts
export default defineConfig({
entry: ['src/entry1.ts', 'src/entry2.ts'],
})
```
#### Named Entries (Object)
```ts
export default defineConfig({
entry: {
main: 'src/index.ts',
utils: 'src/utils.ts',
cli: 'src/cli.ts',
},
})
```
Output files will match the keys:
- `dist/main.mjs`
- `dist/utils.mjs`
- `dist/cli.mjs`
## Glob Patterns
Match multiple files dynamically using glob patterns:
### All TypeScript Files
```ts
export default defineConfig({
entry: 'src/**/*.ts',
})
```
### Exclude Test Files
```ts
export default defineConfig({
entry: ['src/*.ts', '!src/*.test.ts'],
})
```
### Object Entries with Glob Patterns
Use glob wildcards (`*`) in both keys and values. The `*` in the key acts as a placeholder replaced with the matched file name (without extension):
```ts
export default defineConfig({
entry: {
// Maps src/foo.ts → dist/lib/foo.js, src/bar.ts → dist/lib/bar.js
'lib/*': 'src/*.ts',
},
})
```
#### Negation Patterns in Object Entries
Values can be an array with negation patterns (`!`):
```ts
export default defineConfig({
entry: {
'hooks/*': ['src/hooks/*.ts', '!src/hooks/index.ts'],
},
})
```
Multiple positive and negation patterns:
```ts
export default defineConfig({
entry: {
'utils/*': [
'src/utils/*.ts',
'src/utils/*.tsx',
'!src/utils/index.ts',
'!src/utils/internal.ts',
],
},
})
```
**Warning:** Multiple positive patterns in an array value must share the same base directory.
### Mixed Entries
Mix strings, glob patterns, and object entries in an array:
```ts
export default defineConfig({
entry: [
'src/*',
'!src/foo.ts',
{ main: 'index.ts' },
{ 'lib/*': ['src/*.ts', '!src/bar.ts'] },
],
})
```
Object entries take precedence when output names conflict.
### Windows Compatibility
Use forward slashes `/` instead of backslashes `\` on Windows:
```ts
// ✅ Correct
entry: 'src/utils/*.ts'
// ❌ Wrong on Windows
entry: 'src\\utils\\*.ts'
```
## Common Patterns
### Library with Main Export
```ts
export default defineConfig({
entry: 'src/index.ts',
format: ['esm', 'cjs'],
dts: true,
})
```
### Library with Multiple Exports
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
client: 'src/client.ts',
server: 'src/server.ts',
},
format: ['esm', 'cjs'],
dts: true,
})
```
### CLI Tool
```ts
export default defineConfig({
entry: {
cli: 'src/cli.ts',
},
format: ['esm'],
platform: 'node',
})
```
### Preserve Directory Structure
Use with `unbundle: true` to keep file structure:
```ts
export default defineConfig({
entry: ['src/**/*.ts', '!**/*.test.ts'],
unbundle: true,
format: ['esm'],
dts: true,
})
```
This will output files matching the source structure:
- `src/index.ts``dist/index.mjs`
- `src/utils/helper.ts``dist/utils/helper.mjs`
## Tips
1. **Use glob patterns** for multiple related files
2. **Use object syntax** for custom output names
3. **Exclude test files** with negation patterns `!**/*.test.ts`
4. **Combine with unbundle** to preserve directory structure
5. **Use named entries** for better control over output filenames
@@ -0,0 +1,120 @@
# Executable - `exe`
**[experimental]** Bundle as a standalone executable using [Node.js Single Executable Applications](https://nodejs.org/api/single-executable-applications.html).
## Requirements
- Node.js >= 25.5.0 (ESM support requires >= 25.7.0)
- Not supported in Bun or Deno
## Basic Usage
```ts
export default defineConfig({
entry: ['src/cli.ts'],
exe: true,
})
```
## Behavior When Enabled
- Default output format changes from `esm` to `cjs` (unless Node.js >= 25.7.0)
- Declaration file generation (`dts`) is disabled by default
- Code splitting is disabled
- Only single entry points are supported
- Legacy CJS warnings are suppressed
## Advanced Configuration
```ts
export default defineConfig({
entry: ['src/cli.ts'],
exe: {
fileName: 'my-tool',
seaConfig: {
disableExperimentalSEAWarning: true,
useCodeCache: true,
useSnapshot: false,
},
},
})
```
## `ExeOptions`
| Option | Type | Description |
|--------|------|-------------|
| `seaConfig` | `Omit<SeaConfig, 'main' \| 'output' \| 'mainFormat'>` | Node.js configuration options |
| `fileName` | `string \| ((chunk) => string)` | Custom output file name (without `.exe` or platform suffixes) |
| `targets` | `ExeTarget[]` | Cross-platform build targets (requires `@tsdown/exe`) |
## `SeaConfig`
See [Node.js Single Executable Applications documentation](https://nodejs.org/api/single-executable-applications.html).
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `disableExperimentalSEAWarning` | `boolean` | `true` | Disable the experimental warning |
| `useSnapshot` | `boolean` | `false` | Use V8 snapshot |
| `useCodeCache` | `boolean` | `false` | Use V8 code cache |
| `execArgv` | `string[]` | - | Extra Node.js arguments |
| `execArgvExtension` | `'none' \| 'env' \| 'cli'` | `'env'` | How to extend execArgv |
| `assets` | `Record<string, string>` | - | Assets to embed |
## Cross-Platform Builds
Install `@tsdown/exe` to build executables for multiple platforms from a single machine:
```bash
pnpm add -D @tsdown/exe
```
```ts
export default defineConfig({
entry: ['src/cli.ts'],
exe: {
targets: [
{ platform: 'linux', arch: 'x64', nodeVersion: '25.7.0' },
{ platform: 'darwin', arch: 'arm64', nodeVersion: '25.7.0' },
{ platform: 'win', arch: 'x64', nodeVersion: '25.7.0' },
],
},
})
```
This downloads the target platform's Node.js binary, caches it locally, and produces platform-suffixed output:
```
dist/
cli-linux-x64
cli-darwin-arm64
cli-win-x64.exe
```
### `ExeTarget`
| Field | Type | Description |
|-------|------|-------------|
| `platform` | `'win' \| 'darwin' \| 'linux'` | Target OS (nodejs.org naming) |
| `arch` | `'x64' \| 'arm64'` | Target CPU architecture |
| `nodeVersion` | `string` | Node.js version (must be `>=25.7.0`) |
### Caching
Downloaded Node.js binaries are cached in system cache directories:
- **macOS:** `~/Library/Caches/tsdown/node/`
- **Linux:** `~/.cache/tsdown/node/`
- **Windows:** `%LOCALAPPDATA%/tsdown/Caches/node/`
## Platform Notes
- On macOS, the executable is automatically codesigned (ad-hoc) for Gatekeeper compatibility
- On Windows, the `.exe` extension is automatically appended
- When `targets` is specified, `seaConfig.executable` is ignored
## CLI
```bash
tsdown --exe
tsdown src/cli.ts --exe
```
@@ -0,0 +1,127 @@
# Package Validation (publint & attw)
Validate your package configuration and type declarations before publishing.
## Overview
tsdown integrates with [publint](https://publint.dev/) and [Are the types wrong?](https://arethetypeswrong.github.io/) (attw) to catch common packaging issues. Both are optional dependencies.
## Installation
```bash
# publint only
npm install -D publint
# attw only
npm install -D @arethetypeswrong/core
# both
npm install -D publint @arethetypeswrong/core
```
## publint
Checks that `package.json` fields (`exports`, `main`, `module`, `types`) match your actual output files.
### Enable
```ts
export default defineConfig({
publint: true,
})
```
### Configuration
```ts
export default defineConfig({
publint: {
level: 'error', // 'warning' | 'error' | 'suggestion'
},
})
```
### CLI
```bash
tsdown --publint
```
## attw (Are the types wrong?)
Verifies TypeScript declarations are correct across different module resolution strategies (`node10`, `node16`, `bundler`).
### Enable
```ts
export default defineConfig({
attw: true,
})
```
### Configuration
```ts
export default defineConfig({
attw: {
profile: 'node16', // 'strict' | 'node16' | 'esm-only'
level: 'error', // 'warn' | 'error'
ignoreRules: ['false-cjs', 'cjs-resolves-to-esm'],
},
})
```
### Profiles
| Profile | Description |
|---------|-------------|
| `strict` | Requires all resolutions to pass (default) |
| `node16` | Ignores `node10` resolution failures |
| `esm-only` | Ignores `node10` and `node16-cjs` resolution failures |
### Ignore Rules
Suppress specific problem types with `ignoreRules`:
| Rule | Description |
|------|-------------|
| `no-resolution` | Module could not be resolved |
| `untyped-resolution` | Resolution succeeded but has no types |
| `false-cjs` | Types indicate CJS but implementation is ESM |
| `false-esm` | Types indicate ESM but implementation is CJS |
| `cjs-resolves-to-esm` | CJS resolution points to an ESM module |
| `fallback-condition` | A fallback/wildcard condition was used |
| `cjs-only-exports-default` | CJS module only exports a default |
| `named-exports` | Named exports mismatch between types and implementation |
| `false-export-default` | Types declare a default export that doesn't exist |
| `missing-export-equals` | Types are missing `export =` for CJS |
| `unexpected-module-syntax` | File uses unexpected module syntax |
| `internal-resolution-error` | Internal resolution error in type checking |
### CLI
```bash
tsdown --attw
```
## CI Integration
Both tools support CI-aware options:
```ts
export default defineConfig({
publint: 'ci-only',
attw: {
enabled: 'ci-only',
profile: 'node16',
level: 'error',
},
})
```
Both tools require a `package.json` in your project directory.
## Related Options
- [CI Environment](advanced-ci.md) - CI-aware option details
- [Package Exports](option-package-exports.md) - Generate exports field
@@ -0,0 +1,91 @@
# Log Level
Control the verbosity of build output.
## Overview
The `logLevel` option controls how much information tsdown displays during the build process.
## Type
```ts
logLevel?: 'silent' | 'error' | 'warn' | 'info'
```
**Default:** `'info'`
## Basic Usage
### CLI
```bash
# Suppress all output
tsdown --log-level silent
# Only show errors
tsdown --log-level error
# Show warnings and errors
tsdown --log-level warn
# Show all info (default)
tsdown --log-level info
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
logLevel: 'error',
})
```
## Available Levels
| Level | Shows | Use Case |
|-------|-------|----------|
| `silent` | Nothing | CI/CD pipelines, scripting |
| `error` | Errors only | Minimal output |
| `warn` | Warnings + errors | Standard CI/CD |
| `info` | All messages | Development (default) |
## Common Patterns
### CI/CD Pipeline
```ts
export default defineConfig({
entry: ['src/index.ts'],
logLevel: 'error', // Only show errors in CI
})
```
### Scripting
```ts
export default defineConfig({
entry: ['src/index.ts'],
logLevel: 'silent', // No output for automation
})
```
## Fail on Warnings
The `failOnWarn` option controls whether warnings cause the build to exit with a non-zero code. Defaults to `false` — warnings never fail the build.
```ts
export default defineConfig({
failOnWarn: false, // Default: never fail on warnings
// failOnWarn: true, // Always fail on warnings
// failOnWarn: 'ci-only', // Fail on warnings only in CI
})
```
See [CI Environment](advanced-ci.md) for more about CI-aware options.
## Related Options
- [CI Environment](advanced-ci.md) - CI-aware option details
- [CLI Reference](reference-cli.md) - All CLI options
- [Config File](option-config-file.md) - Configuration setup
@@ -0,0 +1,177 @@
# Minification
Compress code to reduce bundle size.
## Overview
Minification removes unnecessary characters (whitespace, comments) and optimizes code for production, reducing bundle size and improving load times.
**Note:** Uses [Oxc minifier](https://oxc.rs/docs/contribute/minifier) internally. The minifier is currently in alpha.
## Type
```ts
minify?: boolean | 'dce-only' | MinifyOptions
```
- `true` — Enable full minification (whitespace removal, mangling, compression)
- `false` — Disable minification (default)
- `'dce-only'` — Only perform dead code elimination without full minification
- `MinifyOptions` — Pass detailed options to the Oxc minifier
## Basic Usage
### CLI
```bash
# Enable minification
tsdown --minify
# Disable minification
tsdown --no-minify
```
**Note:** The CLI `--minify` flag is a boolean toggle. For `'dce-only'` mode or advanced options, use the config file.
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
minify: true,
})
```
### DCE-Only Mode
Remove dead code without full minification (keeps readable output):
```ts
export default defineConfig({
entry: ['src/index.ts'],
minify: 'dce-only',
})
```
## Example Output
### Without Minification
```js
// dist/index.mjs
const x = 1
function hello(x$1) {
console.log('Hello World')
console.log(x$1)
}
hello(x)
```
### With Minification
```js
// dist/index.mjs
const e=1;function t(e){console.log(`Hello World`),console.log(e)}t(e);
```
## Common Patterns
### Production Build
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
minify: true,
clean: true,
})
```
### Conditional Minification
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
format: ['esm'],
minify: !options.watch, // Only minify in production
}))
```
### Browser Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['iife'],
platform: 'browser',
globalName: 'MyLib',
minify: true,
})
```
### Multiple Builds
```ts
export default defineConfig([
// Development build
{
entry: ['src/index.ts'],
format: ['esm'],
minify: false,
outDir: 'dist/dev',
},
// Production build
{
entry: ['src/index.ts'],
format: ['esm'],
minify: true,
outDir: 'dist/prod',
},
])
```
## CLI Examples
```bash
# Production build with minification
tsdown --minify --clean
# Multiple formats with minification
tsdown --format esm --format cjs --minify
# Conditional minification (only when not watching)
tsdown --minify # Or omit --watch
```
## Tips
1. **Use `minify: true`** for production builds
2. **Use `'dce-only'`** to remove dead code while keeping output readable
3. **Skip minification** during development for faster rebuilds
4. **Combine with tree shaking** for best results
5. **Test minified output** thoroughly (Oxc minifier is in alpha)
## Troubleshooting
### Minified Code Has Bugs
Oxc minifier is in alpha and may have issues:
1. **Use DCE-only mode**: `minify: 'dce-only'`
2. **Report bug** to [Oxc project](https://github.com/oxc-project/oxc/issues)
3. **Disable minification**: `minify: false`
### Unexpected Output
- **Test unminified** first to isolate issue
- **Check source maps** for debugging
- **Verify target compatibility**
## Related Options
- [Tree Shaking](option-tree-shaking.md) - Remove unused code
- [Target](option-target.md) - Syntax transformations
- [Output Format](option-output-format.md) - Module formats
- [Sourcemap](option-sourcemap.md) - Debug information
@@ -0,0 +1,272 @@
# Output Directory
Configure the output directory for bundled files.
## Overview
By default, tsdown outputs bundled files to the `dist` directory. You can customize this location using the `outDir` option.
## Basic Usage
### CLI
```bash
# Default output to dist/
tsdown
# Custom output directory
tsdown --out-dir build
tsdown -d lib
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'build',
})
```
## Common Patterns
### Standard Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
outDir: 'dist', // Default
dts: true,
})
```
**Output:**
```
dist/
├── index.mjs
├── index.cjs
└── index.d.ts
```
### Separate Directories by Format
```ts
export default defineConfig([
{
entry: ['src/index.ts'],
format: ['esm'],
outDir: 'dist/esm',
},
{
entry: ['src/index.ts'],
format: ['cjs'],
outDir: 'dist/cjs',
},
])
```
**Output:**
```
dist/
├── esm/
│ └── index.js
└── cjs/
└── index.js
```
### Monorepo Package
```ts
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'lib', // Custom directory
clean: true,
})
```
### Build to Root
```ts
export default defineConfig({
entry: ['src/index.ts'],
outDir: '.', // Output to project root (not recommended)
clean: false, // Don't clean root!
})
```
**Warning:** Be careful when outputting to root to avoid deleting important files.
## Output Extensions
### Custom Extensions
Use `outExtensions` to control file extensions:
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
outDir: 'dist',
outExtensions({ format }) {
return {
js: format === 'esm' ? '.mjs' : '.cjs',
}
},
})
```
### Default Extensions
| Format | Default Extension | With `type: "module"` |
|--------|-------------------|----------------------|
| `esm` | `.mjs` | `.js` |
| `cjs` | `.cjs` | `.js` |
| `iife` | `.iife.js` | `.iife.js` |
| `umd` | `.umd.js` | `.umd.js` |
For IIFE/UMD builds, `outExtensions` customizes extensions or suffixes but does not remove the built-in `.iife` or `.umd` segment. Use `outputOptions.entryFileNames` for custom full filename patterns.
### ESM with .js Extension
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
outExtensions: () => ({ js: '.js' }),
})
```
Requires `"type": "module"` in package.json.
## File Naming
### Entry Names
Control output filenames based on entry names:
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
utils: 'src/utils.ts',
},
outDir: 'dist',
})
```
**Output:**
```
dist/
├── index.mjs
└── utils.mjs
```
### Glob Entry
```ts
export default defineConfig({
entry: ['src/**/*.ts', '!**/*.test.ts'],
outDir: 'dist',
unbundle: true, // Preserve structure
})
```
**Output:**
```
dist/
├── index.mjs
├── utils/
│ └── helper.mjs
└── components/
└── button.mjs
```
## Multiple Builds
### Same Output Directory
```ts
export default defineConfig([
{
entry: ['src/index.ts'],
outDir: 'dist',
clean: true, // Clean first
},
{
entry: ['src/cli.ts'],
outDir: 'dist',
clean: false, // Don't clean again
},
])
```
### Different Output Directories
```ts
export default defineConfig([
{
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
outDir: 'dist/lib',
},
{
entry: ['src/cli.ts'],
format: ['esm'],
outDir: 'dist/bin',
},
])
```
## CLI Examples
```bash
# Default
tsdown
# Custom directory
tsdown --out-dir build
tsdown -d lib
# Nested directory
tsdown --out-dir dist/lib
# With other options
tsdown --out-dir build --format esm,cjs --dts
```
## Tips
1. **Use default `dist`** for standard projects
2. **Be careful with root** - avoid `outDir: '.'`
3. **Clean before build** - use `clean: true`
4. **Consistent naming** - match your project conventions
5. **Separate by format** if needed for clarity
6. **Check .gitignore** - ensure output dir is ignored
## Troubleshooting
### Files Not in Expected Location
- Check `outDir` config
- Verify build completed successfully
- Look for typos in path
### Files Deleted Unexpectedly
- Check if `clean: true`
- Ensure outDir doesn't overlap with source
- Don't use root as outDir
### Permission Errors
- Check write permissions
- Ensure directory isn't locked
- Try different location
## Related Options
- [Cleaning](option-cleaning.md) - Clean output directory
- [Entry](option-entry.md) - Entry points
- [Output Format](option-output-format.md) - Module formats
- [Unbundle](option-unbundle.md) - Preserve structure
@@ -0,0 +1,183 @@
# Output Format
Configure the module format(s) for generated bundles.
## Overview
tsdown can generate bundles in multiple formats. Default is ESM.
## Available Formats
| Format | Description | Use Case |
|--------|-------------|----------|
| `esm` | ECMAScript Module (default) | Modern Node.js, browsers, Deno |
| `cjs` | CommonJS | Legacy Node.js, require() |
| `iife` | Immediately Invoked Function Expression | Browser `<script>` tags |
| `umd` | Universal Module Definition | AMD, CommonJS, and globals |
## Usage
### CLI
```bash
# Single format
tsdown --format esm
# Multiple formats
tsdown --format esm --format cjs
# Or comma-separated
tsdown --format esm,cjs
```
### Config File
#### Single Format
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: 'esm',
})
```
#### Multiple Formats
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
})
```
## Per-Format Configuration
Override options for specific formats:
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: {
esm: {
target: ['es2015'],
},
cjs: {
target: ['node20'],
},
},
})
```
This allows different targets, platforms, or other settings per format.
## Common Patterns
### Modern Library (ESM + CJS)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
})
```
Output:
- `dist/index.mjs` (ESM)
- `dist/index.cjs` (CJS)
- `dist/index.d.ts` (Types)
### Browser Library (IIFE)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['iife'],
globalName: 'MyLib',
platform: 'browser',
minify: true,
})
```
Output: `dist/index.iife.js` (IIFE with global `MyLib`)
### Universal Library (UMD)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['umd'],
globalName: 'MyLib',
platform: 'neutral',
})
```
Works with AMD, CommonJS, and browser globals.
### Node.js Package (CJS + ESM)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
platform: 'node',
dts: true,
shims: true, // Add __dirname, __filename for CJS compat
})
```
### Framework Component Library
```ts
export default defineConfig({
entry: ['src/index.tsx'],
format: ['esm', 'cjs'],
deps: {
neverBundle: ['react', 'react-dom'], // Don't bundle dependencies
},
dts: true,
})
```
## Format-Specific Outputs
### File Extensions
| Format | Extension |
|--------|-----------|
| ESM | `.mjs` or `.js` (with `"type": "module"`) |
| CJS | `.cjs` or `.js` (without `"type": "module"`) |
| IIFE | `.iife.js` |
| UMD | `.umd.js` |
For custom IIFE filenames, set `outputOptions.entryFileNames`. `outExtensions` customizes extensions or suffixes but does not remove `.iife` or `.umd`.
### Customize Extensions
Use `outExtensions` to override:
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
outExtensions: ({ format }) => ({
js: format === 'esm' ? '.js' : '.cjs',
}),
})
```
## Tips
1. **Use ESM + CJS** for maximum compatibility
2. **Use IIFE** for browser-only libraries
3. **Use UMD** for universal compatibility (less common now)
4. **Externalize dependencies** to avoid bundling framework code
5. **Add shims** for CJS compatibility when using Node.js APIs
6. **Set globalName** for IIFE/UMD formats
## Related Options
- [Target](option-target.md) - Set JavaScript version
- [Platform](option-platform.md) - Set platform (node, browser, neutral)
- [Shims](option-shims.md) - Add ESM/CJS compatibility
- [Output Directory](option-output-directory.md) - Customize output paths
@@ -0,0 +1,330 @@
# Auto-Generate Package Exports
Automatically generate package.json exports from build output.
## Overview
tsdown can automatically infer and generate the `exports` field in your `package.json` based on your build outputs.
Top-level `main`, `module`, and `types` fields are not generated by default. Enable `exports.legacy` if you need those fields for older tools.
Review the generated exports before publishing, or enable publint for validation.
## Basic Usage
### CLI
```bash
tsdown --exports
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
exports: true,
})
```
## What Gets Generated
### Single Entry
**Config:**
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
exports: true,
})
```
**Generated in package.json:**
```json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
```
### Multiple Entries
**Config:**
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
utils: 'src/utils.ts',
},
format: ['esm', 'cjs'],
dts: true,
exports: true,
})
```
**Generated in package.json:**
```json
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
}
}
```
## Export All Files
Include all output files, not just entry points:
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
exports: {
all: true,
},
})
```
**Result:** All `.mjs`, `.cjs`, and `.d.ts` files will be added to exports.
## Legacy Package Fields
Generate top-level `main`, `module`, and `types` fields for older tools:
```ts
export default defineConfig({
exports: {
legacy: true,
},
})
```
These fields are not generated by default.
## Dev-Time Source Linking
### Dev Exports
Link to source files during development:
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
exports: {
devExports: true,
},
})
```
**Generated:**
```json
{
"exports": {
".": "./src/index.ts" // Points to source
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
}
```
**Note:** Supported by pnpm/yarn, not npm.
### Conditional Dev Exports
Use specific condition for dev exports:
```ts
export default defineConfig({
exports: {
devExports: '@my-org/source',
},
})
```
**Generated:**
```json
{
"exports": {
".": {
"@my-org/source": "./src/index.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
```
**Use with TypeScript customConditions:**
```json
// tsconfig.json
{
"compilerOptions": {
"customConditions": ["@my-org/source"]
}
}
```
## Custom Exports
Add custom export mappings:
```ts
export default defineConfig({
entry: ['src/index.ts'],
exports: {
customExports(pkg, context) {
// Add custom export
pkg['./foo'] = './dist/foo.js'
// Add package.json export
pkg['./package.json'] = './package.json'
return pkg
},
},
})
```
## Common Patterns
### Complete Library Setup
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
exports: true,
clean: true,
})
```
### Multiple Exports with Dev Mode
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
client: 'src/client.ts',
server: 'src/server.ts',
},
format: ['esm', 'cjs'],
dts: true,
exports: {
all: false, // Only entries
devExports: '@my-org/source',
},
})
```
### Monorepo Package
```ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
exports: true, // Generate for each package
})
```
## Validation
### Enable Publint
Validate generated exports:
```bash
tsdown --exports --publint
```
Or in config:
```ts
export default defineConfig({
exports: true,
publint: true, // Validate exports
})
```
## Tips
1. **Review before publishing** - Check generated fields
2. **Use with publint** - Validate exports field
3. **Enable for libraries** - Especially with multiple exports
4. **Use devExports** - Better DX during development
5. **Test exports** - Verify imports work correctly
## Troubleshooting
### Exports Not Generated
- Ensure `exports: true` is set
- Check build completed successfully
- Verify output files exist
### Wrong Export Paths
- Check `outDir` configuration
- Verify entry names match expectations
- Review `format` settings
### Dev Exports Not Working
- Only supported by pnpm/yarn
- Check package manager
- Use `publishConfig` for publishing
### Types Not Exported
- Enable `dts: true`
- Ensure TypeScript is installed
- Check `.d.ts` files are generated
## CLI Examples
```bash
# Generate exports
tsdown --exports
# With publint validation
tsdown --exports --publint
# Export all files
tsdown --exports
# With dev exports
tsdown --exports
```
## Related Options
- [Entry](option-entry.md) - Configure entry points
- [Output Format](option-output-format.md) - Module formats
- [DTS](option-dts.md) - Type declarations
@@ -0,0 +1,256 @@
# Platform
Target runtime environment for bundled code.
## Overview
Platform determines the runtime environment and affects module resolution, built-in handling, and optimizations.
## Available Platforms
| Platform | Runtime | Built-ins | Use Case |
|----------|---------|-----------|----------|
| `node` | Node.js (default) | Resolved automatically | Server-side, CLIs, tooling |
| `browser` | Web browsers | Warning if used | Front-end applications |
| `neutral` | Platform-agnostic | No assumptions | Universal libraries |
## Usage
### CLI
```bash
tsdown --platform node # Default
tsdown --platform browser
tsdown --platform neutral
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
platform: 'browser',
})
```
## Platform Details
### Node Platform
**Default platform** for server-side and tooling.
```ts
export default defineConfig({
entry: ['src/index.ts'],
platform: 'node',
})
```
**Characteristics:**
- Node.js built-ins (fs, path, etc.) resolved automatically
- Optimized for Node.js runtime
- Compatible with Deno and Bun
- Default mainFields: `['main', 'module']`
### Browser Platform
For web applications running in browsers.
```ts
export default defineConfig({
entry: ['src/index.ts'],
platform: 'browser',
format: ['esm'],
})
```
**Characteristics:**
- Warnings if Node.js built-ins are used
- May require polyfills for Node APIs
- Optimized for browser environments
- Default mainFields: `['browser', 'module', 'main']`
### Neutral Platform
Platform-agnostic for universal libraries.
```ts
export default defineConfig({
entry: ['src/index.ts'],
platform: 'neutral',
format: ['esm'],
})
```
**Characteristics:**
- No runtime assumptions
- No automatic built-in resolution
- Relies on `exports` field only
- Default mainFields: `[]`
- Full control over runtime behavior
## CJS Format Limitation
**CJS format always uses `node` platform** and cannot be changed.
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs'],
platform: 'browser', // Ignored for CJS
})
```
See [rolldown PR #4693](https://github.com/rolldown/rolldown/pull/4693#issuecomment-2912229545) for details.
## Module Resolution
### Main Fields
Different platforms check different `package.json` fields:
| Platform | mainFields | Priority |
|----------|------------|----------|
| `node` | `['main', 'module']` | main → module |
| `browser` | `['browser', 'module', 'main']` | browser → module → main |
| `neutral` | `[]` | Only `exports` field |
### Neutral Platform Resolution
When using `neutral`, packages without `exports` field may fail to resolve:
```
Help: The "main" field here was ignored. Main fields must be configured
explicitly when using the "neutral" platform.
```
**Solution:** Configure mainFields explicitly:
```ts
export default defineConfig({
platform: 'neutral',
inputOptions: {
resolve: {
mainFields: ['module', 'main'],
},
},
})
```
## Common Patterns
### Node.js CLI Tool
```ts
export default defineConfig({
entry: ['src/cli.ts'],
format: ['esm'],
platform: 'node',
shims: true,
})
```
### Browser Library (IIFE)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['iife'],
platform: 'browser',
globalName: 'MyLib',
minify: true,
})
```
### Universal Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
platform: 'neutral',
inputOptions: {
resolve: {
mainFields: ['module', 'main'],
},
},
})
```
### React Component Library
```ts
export default defineConfig({
entry: ['src/index.tsx'],
format: ['esm', 'cjs'],
platform: 'browser',
deps: {
neverBundle: ['react', 'react-dom'],
},
})
```
### Node.js + Browser Builds
```ts
export default defineConfig([
{
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
platform: 'node',
},
{
entry: ['src/browser.ts'],
format: ['esm'],
platform: 'browser',
},
])
```
## Troubleshooting
### Node Built-in Warnings (Browser)
When using Node.js APIs in browser builds:
```
Warning: Module "fs" has been externalized for browser compatibility
```
**Solutions:**
1. Use platform: 'node' if not browser-only
2. Add polyfills for Node APIs
3. Avoid Node.js built-ins in browser code
4. Use platform: 'neutral' with careful dependency management
### Module Resolution Issues (Neutral)
When packages don't resolve with `neutral`:
```ts
export default defineConfig({
platform: 'neutral',
inputOptions: {
resolve: {
mainFields: ['module', 'browser', 'main'],
conditions: ['import', 'require'],
},
},
})
```
## Tips
1. **Use `node`** for server-side and CLIs (default)
2. **Use `browser`** for front-end applications
3. **Use `neutral`** for universal libraries
4. **Configure mainFields** when using neutral platform
5. **CJS is always node** - use ESM for other platforms
6. **Test in target environment** to verify compatibility
## Related Options
- [Output Format](option-output-format.md) - Module formats
- [Target](option-target.md) - JavaScript version
- [Shims](option-shims.md) - ESM/CJS compatibility
- [Dependencies](option-dependencies.md) - External packages
@@ -0,0 +1,88 @@
# Root Directory
Specify the root directory of input files for output structure mapping.
## Overview
The `root` option is similar to TypeScript's `rootDir`. It determines how entry file paths map to output paths. By default, tsdown computes the root as the common base directory of all entry files. Setting `root` explicitly lets you override this behavior.
## Basic Usage
### CLI
```bash
tsdown --root src
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts', 'src/utils/helper.ts'],
root: 'src',
})
```
## How It Works
### Default
Given entries `src/index.ts` and `src/utils/helper.ts`, the common base directory is `src/`:
```
dist/
├── index.js
└── utils/
└── helper.js
```
### With `root: '.'`
Setting root to the project directory preserves the `src/` prefix:
```
dist/
└── src/
├── index.js
└── utils/
└── helper.js
```
## What It Affects
1. **Entry name resolution** — Array entry paths are computed relative to `root` for output filenames
2. **Unbundle mode** — Used as `preserveModulesRoot`, controlling output structure when `unbundle: true`
## When to Use
- Auto-computed common base directory doesn't produce desired output structure
- Need to include or exclude directory prefixes in output paths
- Unbundle mode needs specific directory mapping
## Common Patterns
### Library with `src/` Prefix Preserved
```ts
export default defineConfig({
entry: ['src/**/*.ts', '!**/*.test.ts'],
root: '.',
unbundle: true,
})
```
### Monorepo Package
```ts
export default defineConfig({
entry: ['src/index.ts'],
root: 'src',
unbundle: true,
})
```
## Related Options
- [Unbundle](option-unbundle.md) - Preserve directory structure
- [Entry](option-entry.md) - Entry point configuration
- [Output Directory](option-output-directory.md) - Output location
@@ -0,0 +1,299 @@
# Shims
Add compatibility between ESM and CommonJS module systems.
## Overview
Shims provide small pieces of code that bridge the gap between CommonJS (CJS) and ECMAScript Modules (ESM), enabling cross-module-system compatibility.
## What Shims Provide
### ESM Output (when enabled)
With `shims: true`, adds CommonJS variables to ESM:
- `__dirname` - Current directory path
- `__filename` - Current file path
### ESM Output (automatic)
Always added when using `require` in ESM on Node.js:
- `require` function via `createRequire(import.meta.url)`
### CJS Output (automatic)
Always added to CommonJS output:
- `import.meta.url`
- `import.meta.dirname`
- `import.meta.filename`
## Usage
### CLI
```bash
tsdown --shims
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
shims: true,
})
```
## Generated Code
### ESM with Shims
**Source:**
```ts
console.log(__dirname)
console.log(__filename)
```
**Output (shims: true):**
```js
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__dirname)
console.log(__filename)
```
### ESM with require
**Source:**
```ts
const mod = require('some-module')
```
**Output (automatic on Node.js):**
```js
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const mod = require('some-module')
```
### CJS with import.meta
**Source:**
```ts
console.log(import.meta.url)
console.log(import.meta.dirname)
```
**Output (automatic):**
```js
const import_meta = {
url: require('url').pathToFileURL(__filename).toString(),
dirname: __dirname,
filename: __filename
}
console.log(import_meta.url)
console.log(import_meta.dirname)
```
## Common Patterns
### Node.js CLI Tool
```ts
export default defineConfig({
entry: ['src/cli.ts'],
format: ['esm'],
platform: 'node',
shims: true, // Add __dirname, __filename
})
```
### Dual Format Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
platform: 'node',
shims: true, // ESM gets __dirname/__filename
// CJS gets import.meta.* (automatic)
})
```
### Server-Side Code
```ts
export default defineConfig({
entry: ['src/server.ts'],
format: ['esm'],
platform: 'node',
shims: true,
deps: {
neverBundle: [/.*/], // External all deps
},
})
```
### File System Operations
```ts
// Source code
import { readFileSync } from 'fs'
import { join } from 'path'
// Read file relative to current module
const content = readFileSync(join(__dirname, 'data.json'), 'utf-8')
```
```ts
// tsdown config
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
shims: true, // Enables __dirname
})
```
## When to Use Shims
### Use `shims: true` when:
- ✅ Building Node.js tools/CLIs
- ✅ Code uses `__dirname` or `__filename`
- ✅ Need file system operations relative to module
- ✅ Migrating from CommonJS to ESM
- ✅ Need cross-format compatibility
### Don't need shims when:
- ❌ Browser-only code
- ❌ No file system operations
- ❌ Using only `import.meta.url`
- ❌ Pure ESM without CJS variables
## Performance Impact
### Runtime Overhead
Shims add minimal runtime overhead:
```js
// Added to output when shims enabled
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
```
### Tree Shaking
If `__dirname` or `__filename` are not used, they're automatically removed during bundling (no overhead).
## Platform Considerations
### Node.js Platform
```ts
export default defineConfig({
platform: 'node',
format: ['esm'],
shims: true, // Recommended for Node.js
})
```
- `require` shim added automatically
- `__dirname` and `__filename` available with `shims: true`
### Browser Platform
```ts
export default defineConfig({
platform: 'browser',
format: ['esm'],
shims: false, // Not needed for browser
})
```
- Shims not needed (no Node.js variables)
- Will cause warnings if Node.js APIs used
### Neutral Platform
```ts
export default defineConfig({
platform: 'neutral',
format: ['esm'],
shims: false, // Avoid platform-specific code
})
```
- Avoid shims for maximum portability
## CLI Examples
```bash
# Enable shims
tsdown --shims
# ESM with shims for Node.js
tsdown --format esm --platform node --shims
# Dual format with shims
tsdown --format esm --format cjs --shims
```
## Troubleshooting
### `__dirname is not defined`
Enable shims:
```ts
export default defineConfig({
shims: true,
})
```
### `require is not defined` in ESM
Automatic on Node.js platform. If not working:
```ts
export default defineConfig({
platform: 'node', // Ensure Node.js platform
})
```
### Import.meta not working in CJS
Automatic - no configuration needed. If still failing, check output format:
```ts
export default defineConfig({
format: ['cjs'], // Shims added automatically
})
```
## Tips
1. **Enable for Node.js tools** - Use `shims: true` for CLIs and servers
2. **Skip for browsers** - Not needed for browser code
3. **No overhead if unused** - Automatically tree-shaken
4. **Automatic require shim** - No config needed for `require` in ESM
5. **CJS shims automatic** - `import.meta.*` always available in CJS
## Related Options
- [Platform](option-platform.md) - Runtime environment
- [Output Format](option-output-format.md) - Module formats
- [Target](option-target.md) - Syntax transformations
@@ -0,0 +1,301 @@
# Source Maps
Generate source maps for debugging bundled code.
## Overview
Source maps map minified/bundled code back to original source files, making debugging significantly easier by showing original line numbers and variable names.
## Basic Usage
### CLI
```bash
tsdown --sourcemap
# Or inline
tsdown --sourcemap inline
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
sourcemap: true,
})
```
## Source Map Types
### External (default)
Generates separate `.map` files:
```ts
export default defineConfig({
sourcemap: true, // or 'external'
})
```
**Output:**
- `dist/index.mjs`
- `dist/index.mjs.map`
**Pros:**
- Smaller bundle size
- Can be excluded from production
- Faster parsing
### Inline
Embeds source maps in the bundle:
```ts
export default defineConfig({
sourcemap: 'inline',
})
```
**Output:**
- `dist/index.mjs` (includes source map as data URL)
**Pros:**
- Single file deployment
- Guaranteed to be available
**Cons:**
- Larger bundle size
- Exposed in production
### Hidden
Generates map files without reference comment:
```ts
export default defineConfig({
sourcemap: 'hidden',
})
```
**Output:**
- `dist/index.mjs` (no `//# sourceMappingURL` comment)
- `dist/index.mjs.map`
**Use when:**
- You want maps for error reporting tools
- But don't want them exposed to users
## Auto-Enable Scenarios
### Declaration Maps
If `declarationMap` is enabled in `tsconfig.json`, source maps are automatically enabled:
```json
// tsconfig.json
{
"compilerOptions": {
"declarationMap": true
}
}
```
This also generates `.d.ts.map` files for TypeScript declarations.
## Common Patterns
### Development Build
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
sourcemap: options.watch, // Only in dev
minify: !options.watch,
}))
```
### Production with External Maps
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
sourcemap: true, // External maps
minify: true,
})
```
Deploy maps to separate error reporting service.
### Always Inline (Development Tool)
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
sourcemap: 'inline',
})
```
### Per-Format Source Maps
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: {
esm: {
sourcemap: true,
},
iife: {
sourcemap: 'inline', // Inline for browser
},
},
})
```
### TypeScript Library with Declaration Maps
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
sourcemap: true,
dts: {
sourcemap: true, // Enable declaration maps
},
})
```
**Output:**
- `dist/index.mjs` + `dist/index.mjs.map`
- `dist/index.cjs` + `dist/index.cjs.map`
- `dist/index.d.ts` + `dist/index.d.ts.map`
## Benefits
### For Development
- **Faster debugging** - See original code in debugger
- **Better error messages** - Stack traces show original lines
- **Easier breakpoints** - Set breakpoints on source code
### For Production
- **Error reporting** - Send accurate error locations to services
- **Monitoring** - Track errors back to source
- **Support** - Help users report issues accurately
## Performance Impact
| Type | Bundle Size | Parse Speed | Debugging |
|------|-------------|-------------|-----------|
| None | Smallest | Fastest | Hard |
| External | Small | Fast | Easy |
| Inline | Largest | Slower | Easy |
| Hidden | Small | Fast | Tools only |
## CLI Examples
```bash
# Enable source maps
tsdown --sourcemap
# Inline source maps
tsdown --sourcemap inline
# Hidden source maps
tsdown --sourcemap hidden
# Development with source maps
tsdown --watch --sourcemap
# Production with external maps
tsdown --minify --sourcemap
# No source maps
tsdown --no-sourcemap
```
## Use Cases
### Local Development
```ts
export default defineConfig({
sourcemap: true,
minify: false,
})
```
### Production Build
```ts
export default defineConfig({
sourcemap: 'external', // Upload to error service
minify: true,
})
```
### Browser Library
```ts
export default defineConfig({
format: ['iife'],
platform: 'browser',
sourcemap: 'inline', // Self-contained
globalName: 'MyLib',
})
```
### Node.js CLI Tool
```ts
export default defineConfig({
format: ['esm'],
platform: 'node',
sourcemap: true,
shims: true,
})
```
## Troubleshooting
### Source Maps Not Working
1. **Check output** - Verify `.map` files are generated
2. **Check reference** - Look for `//# sourceMappingURL=` comment
3. **Check paths** - Ensure relative paths are correct
4. **Check tool** - Verify debugger/browser supports source maps
### Large Bundle Size
Use external source maps instead of inline:
```ts
export default defineConfig({
sourcemap: true, // Not 'inline'
})
```
### Source Not Found
- Ensure source files are accessible relative to map
- Check `sourceRoot` in generated map
- Verify paths in `sources` array
## Tips
1. **Use external maps** for production (smaller bundles)
2. **Use inline maps** for single-file tools
3. **Enable in development** for better DX
4. **Upload to error services** for production debugging
5. **Use hidden maps** when you want them for tools only
6. **Enable declaration maps** for TypeScript libraries
## Related Options
- [Minification](option-minification.md) - Code compression
- [DTS](option-dts.md) - TypeScript declarations
- [Watch Mode](option-watch-mode.md) - Development workflow
- [Target](option-target.md) - Syntax transformations
@@ -0,0 +1,222 @@
# Target Environment
Configure JavaScript syntax transformations for target environments.
## Overview
The `target` option controls which JavaScript features are downleveled (transformed to older syntax) for compatibility.
**Important:** Only affects syntax transformations, not runtime polyfills.
## Default Behavior
tsdown auto-reads from `package.json`:
```json
// package.json
{
"engines": {
"node": ">=18.0.0"
}
}
```
Automatically sets `target` to `node18.0.0`.
If no `engines.node` field exists, behaves as if `target: false` (no transformations).
## Disabling Transformations
Set to `false` to preserve modern syntax:
```ts
export default defineConfig({
target: false,
})
```
**Result:**
- No JavaScript downleveling
- Modern features preserved (optional chaining `?.`, nullish coalescing `??`, etc.)
**Use when:**
- Targeting modern environments
- Handling transformations elsewhere
- Building libraries for further processing
## Setting Target
### CLI
```bash
# Single target
tsdown --target es2020
tsdown --target node20
# Multiple targets
tsdown --target chrome100 --target node20.18
# Disable
tsdown --no-target
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
target: 'es2020',
})
```
### Multiple Targets
```ts
export default defineConfig({
entry: ['src/index.ts'],
target: ['chrome100', 'safari15', 'node18'],
})
```
## Supported Targets
### ECMAScript Versions
- `es2015`, `es2016`, `es2017`, `es2018`, `es2019`, `es2020`, `es2021`, `es2022`, `es2023`, `esnext`
### Browser Versions
- `chrome100`, `safari18`, `firefox110`, `edge100`, etc.
### Node.js Versions
- `node16`, `node18`, `node20`, `node20.18`, etc.
## Examples
### Modern Browsers
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: ['chrome100', 'safari15', 'firefox100'],
})
```
### Node.js Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
target: 'node18',
})
```
### Legacy Support
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: 'es2015', // Maximum compatibility
})
```
### Per-Format Targets
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: {
esm: {
target: 'es2020',
},
cjs: {
target: 'node16',
},
},
})
```
## Decorators
### Legacy Decorators (Stage 2)
Enable in `tsconfig.json`:
```json
{
"compilerOptions": {
"experimentalDecorators": true
}
}
```
### Stage 3 Decorators
**Not currently supported** by tsdown/Rolldown/Oxc.
See [oxc issue #9170](https://github.com/oxc-project/oxc/issues/9170).
## Common Patterns
### Universal Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
target: 'es2020', // Wide compatibility
})
```
### Modern-Only Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: false, // No transformations
})
```
### Browser Component
```ts
export default defineConfig({
entry: ['src/index.tsx'],
format: ['esm'],
target: ['chrome100', 'safari15', 'firefox100'],
platform: 'browser',
})
```
## CSS Targeting
When `@tsdown/css` is installed and a browser target is set, CSS syntax is also lowered automatically:
```ts
export default defineConfig({
target: 'chrome108', // CSS nesting will be flattened
})
```
See [CSS](option-css.md) for full CSS configuration options.
## Tips
1. **Let tsdown auto-detect** from package.json when possible
2. **Use `false`** for modern-only builds
3. **Specify multiple targets** for broader compatibility
4. **Use legacy decorators** with `experimentalDecorators`
5. **Install `@tsdown/css`** for CSS support and syntax lowering
6. **Test output** in target environments
## Related Options
- [Platform](option-platform.md) - Runtime environment
- [Output Format](option-output-format.md) - Module formats
- [Minification](option-minification.md) - Code optimization
- [CSS](option-css.md) - CSS handling and preprocessors
@@ -0,0 +1,335 @@
# Tree Shaking
Remove unused code from bundles.
## Overview
Tree shaking eliminates dead code (unused exports) from your final bundle, reducing size and improving performance.
**Default:** Enabled
## Basic Usage
### CLI
```bash
# Tree shaking enabled (default)
tsdown
# Disable tree shaking
tsdown --no-treeshake
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
treeshake: true, // Default
})
```
## How It Works
### With Tree Shaking
**Source:**
```ts
// src/util.ts
export function unused() {
console.log("I'm unused")
}
export function hello(x: number) {
console.log('Hello World', x)
}
// src/index.ts
import { hello } from './util'
hello(1)
```
**Output:**
```js
// dist/index.mjs
function hello(x) {
console.log('Hello World', x)
}
hello(1)
```
`unused()` function is removed because it's never imported.
### Without Tree Shaking
**Output:**
```js
// dist/index.mjs
function unused() {
console.log("I'm unused")
}
function hello(x) {
console.log('Hello World', x)
}
hello(1)
```
All code is included, even if unused.
## Advanced Configuration
### Enable (Default)
```ts
export default defineConfig({
treeshake: true,
})
```
Uses Rolldown's default tree shaking.
### Custom Options
```ts
export default defineConfig({
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
unknownGlobalSideEffects: false,
},
})
```
See [Rolldown docs](https://rolldown.rs/reference/InputOptions.treeshake#treeshake) for all options.
### Disable
```ts
export default defineConfig({
treeshake: false,
})
```
## Side Effects
### Package.json sideEffects
Declare side effects in your package:
```json
{
"sideEffects": false
}
```
Or specify files with side effects:
```json
{
"sideEffects": ["*.css", "src/polyfills.ts"]
}
```
### Module Side Effects
```ts
export default defineConfig({
treeshake: {
moduleSideEffects: (id) => {
// Preserve side effects for polyfills
return id.includes('polyfill')
},
},
})
```
## Common Patterns
### Production Build
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
treeshake: true,
minify: true,
})
```
### Development Build
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
treeshake: !options.watch, // Disable in dev
}))
```
### Library with Side Effects
```ts
export default defineConfig({
entry: ['src/index.ts'],
treeshake: {
moduleSideEffects: (id) => {
return (
id.includes('.css') ||
id.includes('polyfill') ||
id.includes('side-effect')
)
},
},
})
```
### Utilities Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
treeshake: true,
dts: true,
})
```
Users can import only what they need:
```ts
import { onlyWhatINeed } from 'my-utils'
```
## Benefits
### Smaller Bundles
- Only includes imported code
- Removes unused functions, classes, variables
- Reduces download size
### Better Performance
- Less code to parse
- Faster execution
- Improved loading times
### Cleaner Output
- No dead code in production
- Easier to debug
- Better maintainability
## When to Disable
### Debugging
During development to see all code:
```ts
export default defineConfig((options) => ({
treeshake: !options.watch,
}))
```
### Side Effect Code
Code with global side effects:
```ts
// This has side effects
window.myGlobal = {}
export function setup() {
// ...
}
```
Disable tree shaking or mark side effects:
```json
{
"sideEffects": true
}
```
### Testing
Include all code for coverage:
```ts
export default defineConfig({
treeshake: false,
})
```
## Tips
1. **Leave enabled** for production builds
2. **Mark side effects** in package.json
3. **Use with minification** for best results
4. **Test tree shaking** - verify unused code is removed
5. **Disable for debugging** if needed
6. **Pure functions** are easier to tree shake
## Troubleshooting
### Code Still Included
- Check for side effects
- Verify imports are ES modules
- Ensure code is actually unused
- Check `sideEffects` in package.json
### Missing Code at Runtime
- Code has side effects but marked as none
- Set `sideEffects: true` or list specific files
### Unexpected Behavior
- Module has side effects not declared
- Try disabling tree shaking to isolate issue
## Examples
### Pure Utility Functions
```ts
// utils.ts - perfect for tree shaking
export function add(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
// Only 'add' imported = only 'add' bundled
import { add } from './utils'
```
### With Side Effects
```ts
// polyfill.ts - has side effects
if (!Array.prototype.at) {
Array.prototype.at = function(index) {
// polyfill implementation
}
}
export {} // Need to export something
```
```json
{
"sideEffects": ["src/polyfill.ts"]
}
```
## Related Options
- [Minification](option-minification.md) - Code compression
- [Target](option-target.md) - Syntax transformations
- [Dependencies](option-dependencies.md) - External packages
- [Output Format](option-output-format.md) - Module formats
@@ -0,0 +1,310 @@
# Unbundle Mode
Preserve source directory structure in output.
## Overview
Unbundle mode (also called "bundleless" or "transpile-only") outputs files that mirror your source structure, rather than bundling everything into single files. Each source file is compiled individually with a one-to-one mapping.
## Basic Usage
### CLI
```bash
tsdown --unbundle
```
### Config File
```ts
export default defineConfig({
entry: ['src/**/*.ts', '!**/*.test.ts'],
unbundle: true,
})
```
## How It Works
### Source Structure
```
src/
├── index.ts
├── utils/
│ ├── helper.ts
│ └── format.ts
└── components/
└── button.ts
```
### With Unbundle
**Config:**
```ts
export default defineConfig({
entry: ['src/index.ts'],
unbundle: true,
})
```
**Output:**
```
dist/
├── index.mjs
├── utils/
│ ├── helper.mjs
│ └── format.mjs
└── components/
└── button.mjs
```
All imported files are output individually, preserving structure.
### Without Unbundle (Default)
**Output:**
```
dist/
└── index.mjs (all code bundled together)
```
## When to Use
### Use Unbundle When:
✅ Building monorepo packages with shared utilities
✅ Users need to import individual modules
✅ Want clear source-to-output mapping
✅ Library with many independent utilities
✅ Debugging requires tracing specific files
✅ Incremental builds for faster development
### Use Standard Bundling When:
❌ Single entry point application
❌ Want to optimize bundle size
❌ Need aggressive tree shaking
❌ Creating IIFE/UMD bundles
❌ Deploying to browsers directly
## Common Patterns
### Utility Library
```ts
export default defineConfig({
entry: ['src/**/*.ts', '!**/*.test.ts'],
format: ['esm', 'cjs'],
unbundle: true,
dts: true,
})
```
**Benefits:**
- Users import only what they need
- Tree shaking still works at user's build
- Clear module boundaries
**Usage:**
```ts
// Users can import specific utilities
import { helper } from 'my-lib/utils/helper'
import { Button } from 'my-lib/components/button'
```
### Monorepo Shared Package
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
unbundle: true,
outDir: 'dist',
})
```
### TypeScript Compilation Only
```ts
export default defineConfig({
entry: ['src/**/*.ts'],
format: ['esm'],
unbundle: true,
minify: false,
treeshake: false,
dts: true,
})
```
Pure TypeScript to JavaScript transformation.
### Development Mode
```ts
export default defineConfig((options) => ({
entry: ['src/**/*.ts'],
unbundle: options.watch, // Unbundle in dev only
minify: !options.watch,
}))
```
Fast rebuilds during development, optimized for production.
## With Entry Patterns
### Include/Exclude
```ts
export default defineConfig({
entry: [
'src/**/*.ts',
'!**/*.test.ts',
'!**/*.spec.ts',
'!**/fixtures/**',
],
unbundle: true,
})
```
### Multiple Entry Points
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
cli: 'src/cli.ts',
},
unbundle: true,
})
```
Both entry files and all imports preserved.
## Output Control
### Custom Extension
```ts
export default defineConfig({
entry: ['src/**/*.ts'],
unbundle: true,
outExtensions: () => ({ js: '.js' }),
})
```
### Preserve Directory
```ts
export default defineConfig({
entry: ['src/**/*.ts'],
unbundle: true,
outDir: 'lib',
})
```
**Output:**
```
lib/
├── index.js
├── utils/
│ └── helper.js
└── components/
└── button.js
```
## Package.json Setup
```json
{
"name": "my-library",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./utils/*": "./dist/utils/*.js",
"./components/*": "./dist/components/*.js"
},
"files": ["dist"]
}
```
Or use `exports: true` to auto-generate.
## Comparison
| Feature | Bundled | Unbundled |
|---------|---------|-----------|
| Output files | Few | Many |
| File size | Smaller | Larger |
| Build speed | Slower | Faster |
| Tree shaking | Build time | User's build |
| Source mapping | Complex | Simple |
| Module imports | Entry only | Any module |
| Dev rebuilds | Slower | Faster |
## Performance
### Build Speed
Unbundle is typically faster:
- No bundling overhead
- Parallel file processing
- Incremental builds possible
### Bundle Size
Unbundle produces larger output:
- Each file has its own overhead
- No cross-module optimizations
- User's bundler handles final optimization
## Tips
1. **Use with glob patterns** for multiple files
2. **Enable in development** for faster rebuilds
3. **Let users bundle** for production optimization
4. **Preserve structure** for utilities/components
5. **Combine with DTS** for type definitions
6. **Use with monorepos** for shared code
## Troubleshooting
### Too Many Files
- Adjust entry patterns
- Exclude unnecessary files
- Use specific entry points
### Missing Files
- Check entry patterns
- Verify files are imported
- Look for excluded patterns
### Import Paths Wrong
- Check relative paths
- Verify output structure
- Update package.json exports
## CLI Examples
```bash
# Enable unbundle
tsdown --unbundle
# With specific entry
tsdown src/**/*.ts --unbundle
# With other options
tsdown --unbundle --format esm --dts
```
## Related Options
- [Root Directory](option-root.md) - Control output directory mapping
- [Entry](option-entry.md) - Entry patterns
- [Output Directory](option-output-directory.md) - Output location
- [Output Format](option-output-format.md) - Module formats
- [DTS](option-dts.md) - Type declarations
@@ -0,0 +1,261 @@
# Watch Mode
Automatically rebuild when files change.
## Overview
Watch mode monitors your source files and rebuilds automatically on changes, streamlining the development workflow.
## Basic Usage
### CLI
```bash
# Watch all project files
tsdown --watch
# Or use short flag
tsdown -w
# Watch specific directory
tsdown --watch ./src
# Watch specific file
tsdown --watch ./src/index.ts
```
### Config File
```ts
export default defineConfig({
entry: ['src/index.ts'],
watch: true,
})
```
## Watch Options
### Ignore Paths
Ignore specific paths in watch mode:
```bash
tsdown --watch --ignore-watch test --ignore-watch '**/*.test.ts'
```
```ts
export default defineConfig({
entry: ['src/index.ts'],
watch: {
exclude: ['test/**', '**/*.test.ts'],
},
})
```
### On Success Command
Run command after successful build:
```bash
tsdown --watch --on-success "echo Build complete!"
tsdown --watch --on-success "node dist/index.mjs"
```
```ts
export default defineConfig({
entry: ['src/index.ts'],
watch: true,
onSuccess: 'node dist/index.mjs',
})
```
## Watch Behavior
### Default Watch Targets
By default, tsdown watches:
- All entry files
- All imported files
- Config file (triggers restart)
### File Change Handling
- **Source files** - Incremental rebuild
- **Config file** - Full restart with cache clear
- **Dependencies** - Rebuild if imported
### Keyboard Shortcuts
During watch mode:
- `r` - Manual rebuild
- `q` - Quit watch mode
## Common Patterns
### Development Mode
```ts
export default defineConfig((options) => ({
entry: ['src/index.ts'],
format: ['esm'],
watch: options.watch,
sourcemap: options.watch,
minify: !options.watch,
}))
```
### With Post-Build Script
```ts
export default defineConfig({
entry: ['src/index.ts'],
watch: true,
onSuccess: 'npm run test',
})
```
### Multiple Entry Points
```ts
export default defineConfig({
entry: {
main: 'src/index.ts',
cli: 'src/cli.ts',
},
watch: true,
clean: false, // Don't clean on each rebuild
})
```
### Test Runner Integration
```bash
# Watch and run tests on change
tsdown --watch --on-success "vitest run"
# Watch and start dev server
tsdown --watch --on-success "node dist/server.mjs"
```
### Monorepo Package
```ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
watch: true,
watch: {
exclude: ['**/test/**', '**/*.spec.ts'],
},
})
```
## Advanced Configuration
### Custom Watch Options
```ts
export default defineConfig({
entry: ['src/index.ts'],
watch: {
include: ['src/**'],
exclude: ['**/*.test.ts', '**/fixtures/**'],
skipWrite: false,
},
})
```
### Conditional Watch
```ts
export default defineConfig((options) => {
const isDev = options.watch
return {
entry: ['src/index.ts'],
format: ['esm'],
dts: !isDev, // Skip DTS in watch mode
sourcemap: isDev,
clean: !isDev,
}
})
```
## CLI Examples
```bash
# Basic watch
tsdown -w
# Watch with source maps
tsdown -w --sourcemap
# Watch without cleaning
tsdown -w --no-clean
# Watch and run on success
tsdown -w --on-success "npm test"
# Watch specific format
tsdown -w --format esm
# Watch with minification
tsdown -w --minify
# Watch and ignore test files
tsdown -w --ignore-watch '**/*.test.ts'
```
## Tips
1. **Use watch mode** for active development
2. **Skip DTS generation** in watch for faster rebuilds
3. **Disable clean** to avoid unnecessary file operations
4. **Use onSuccess** for post-build tasks
5. **Ignore test files** to avoid unnecessary rebuilds
6. **Use keyboard shortcuts** for manual control
## Troubleshooting
### Watch Not Detecting Changes
- Check file is in entry or imported chain
- Verify path is not in `exclude` patterns
- Ensure file system supports watching
### Too Many Rebuilds
Add ignore patterns:
```ts
export default defineConfig({
watch: {
exclude: [
'**/node_modules/**',
'**/.git/**',
'**/dist/**',
'**/*.test.ts',
],
},
})
```
### Slow Rebuilds
- Skip DTS in watch mode: `dts: !options.watch`
- Disable minification: `minify: false`
- Use smaller entry set during development
### Config Changes Not Applied
Config file changes trigger full restart automatically.
### Why Not Stub Mode?
tsdown does not support stub mode. Watch mode is the recommended alternative for rapid development, providing instant rebuilds without the drawbacks of stub mode.
## Related Options
- [On Success](reference-cli.md#on-success-command) - Post-build commands
- [Sourcemap](option-sourcemap.md) - Debug information
- [Clean](option-cleaning.md) - Output directory cleaning
@@ -0,0 +1,338 @@
# React Support
Build React component libraries with tsdown.
## Overview
tsdown provides first-class support for React libraries. Rolldown natively supports JSX/TSX, so no additional plugins are required for basic React components.
## Quick Start
### Use Starter Template
```bash
# Basic React library
npx create-tsdown@latest -t react
# With React Compiler
npx create-tsdown@latest -t react-compiler
```
## Basic Configuration
### Minimal Setup
```ts
// tsdown.config.ts
export default defineConfig({
entry: ['./src/index.ts'],
format: ['esm', 'cjs'],
platform: 'neutral',
deps: {
neverBundle: ['react', 'react-dom'],
},
dts: true,
})
```
### Component Example
```tsx
// src/MyButton.tsx
import React from 'react'
interface MyButtonProps {
type?: 'primary' | 'secondary'
onClick?: () => void
}
export const MyButton: React.FC<MyButtonProps> = ({ type = 'primary', onClick }) => {
return (
<button className={`btn btn-${type}`} onClick={onClick}>
Click me
</button>
)
}
```
```ts
// src/index.ts
export { MyButton } from './MyButton'
```
## JSX Transform
### Automatic (Default)
Modern JSX transform (React 17+):
```ts
export default defineConfig({
entry: ['src/index.tsx'],
// Automatic JSX is default
})
```
**Characteristics:**
- No `import React` needed
- Smaller bundle size
- React 17+ required
### Classic
Legacy JSX transform:
```ts
export default defineConfig({
entry: ['src/index.tsx'],
inputOptions: {
transform: {
jsx: 'react', // Classic transform
},
},
})
```
**Characteristics:**
- Requires `import React from 'react'`
- Compatible with older React versions
## React Compiler
React Compiler automatically optimizes React code at build time.
### Install Dependencies
```bash
pnpm add -D @rollup/plugin-babel babel-plugin-react-compiler
```
### Configure
```ts
import pluginBabel from '@rollup/plugin-babel'
export default defineConfig({
entry: ['src/index.tsx'],
format: ['esm', 'cjs'],
deps: {
neverBundle: ['react', 'react-dom'],
},
plugins: [
pluginBabel({
babelHelpers: 'bundled',
parserOpts: {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
},
plugins: ['babel-plugin-react-compiler'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}),
],
dts: true,
})
```
## Common Patterns
### Component Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
platform: 'neutral',
deps: {
neverBundle: [
'react',
'react-dom',
/^react\//, // react/jsx-runtime, etc.
],
},
dts: true,
clean: true,
})
```
### Multiple Components
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
Button: 'src/Button.tsx',
Input: 'src/Input.tsx',
Modal: 'src/Modal.tsx',
},
format: ['esm', 'cjs'],
deps: {
neverBundle: ['react', 'react-dom'],
},
dts: true,
})
```
### Hooks Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
platform: 'neutral',
deps: {
neverBundle: ['react'], // Only React needed
},
dts: true,
treeshake: true,
})
```
### Monorepo React Packages
```ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
deps: {
neverBundle: [
'react',
'react-dom',
/^@mycompany\//, // Other workspace packages
],
},
dts: true,
})
```
## TypeScript Configuration
### Recommended tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx", // or "react" for classic
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"strict": true,
"isolatedDeclarations": true, // Fast DTS generation
"skipLibCheck": true
},
"include": ["src"]
}
```
## Package.json Configuration
```json
{
"name": "my-react-library",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"files": ["dist"],
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tsdown": "^0.9.0",
"typescript": "^5.0.0"
}
}
```
## Advanced Patterns
### With Fast Refresh (Development)
```ts
import react from '@vitejs/plugin-react'
export default defineConfig((options) => ({
entry: ['src/index.ts'],
format: ['esm'],
deps: {
neverBundle: ['react', 'react-dom'],
},
plugins: options.watch
? [
// @ts-expect-error Vite plugin
react({ fastRefresh: true }),
]
: [],
}))
```
## Tips
1. **Always externalize React** - Don't bundle React/ReactDOM
2. **Use automatic JSX** - Smaller bundles with React 17+
3. **Enable DTS generation** - TypeScript support essential
4. **Use platform: 'neutral'** - For maximum compatibility
5. **Add peer dependencies** - Let users provide React
6. **Enable tree shaking** - Reduce bundle size
7. **Use React Compiler** - Better runtime performance
## Troubleshooting
### React Hook Errors
Ensure React is externalized:
```ts
deps: {
neverBundle: ['react', 'react-dom', /^react\//],
}
```
### Type Errors with JSX
Check `tsconfig.json`:
```json
{
"compilerOptions": {
"jsx": "react-jsx" // or "react"
}
}
```
### Duplicate React
Add to deps.neverBundle:
```ts
deps: {
neverBundle: [
'react',
'react-dom',
'react/jsx-runtime',
'react/jsx-dev-runtime',
],
}
```
## Related
- [Plugins](advanced-plugins.md) - Extend functionality
- [Dependencies](option-dependencies.md) - External packages
- [DTS](option-dts.md) - Type declarations
- [Vue Recipe](recipe-vue.md) - Vue component libraries
@@ -0,0 +1,42 @@
# Solid Support
Build Solid component libraries with `tsdown` using `unplugin-solid`.
## Quick Start
```bash
npx create-tsdown@latest -t solid
```
## Configuration
```ts
import solid from 'unplugin-solid/rolldown'
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['./src/index.ts'],
platform: 'neutral',
dts: true,
plugins: [solid()],
})
```
## Dependencies
Install `unplugin-solid`:
```bash
npm install -D unplugin-solid
```
## Key Points
- Use `platform: 'neutral'` for framework-agnostic output
- `dts: true` generates TypeScript declarations
- The Solid plugin handles JSX compilation for Solid's reactive system
## Related
- [Plugins](advanced-plugins.md) - Plugin configuration
- [Platform](option-platform.md) - Platform options
@@ -0,0 +1,54 @@
# Svelte Support
Build Svelte component libraries with `tsdown` using `rollup-plugin-svelte`.
## Quick Start
```bash
npx create-tsdown@latest -t svelte
```
## Configuration
```ts
import svelte from 'rollup-plugin-svelte'
import { sveltePreprocess } from 'svelte-preprocess'
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['./src/index.ts'],
platform: 'neutral',
plugins: [svelte({ preprocess: sveltePreprocess() })],
})
```
## Dependencies
```bash
npm install -D rollup-plugin-svelte svelte svelte-preprocess
```
## Distribution Strategy
**Recommended: Ship `.svelte` source files** instead of precompiled JS. Let consumers' tooling (Vite + `@sveltejs/vite-plugin-svelte`) compile in their apps.
Reasons:
- Avoids version compatibility issues with `svelte/internal`
- Better SSR/hydration consistency
- Consumers get better HMR, diagnostics, and tree-shaking
- Fewer republish cycles on Svelte upgrades
**Exceptions** where shipping JS makes sense:
- Web Components via `customElement` mode
- CDN direct-load without a build step
## Key Points
- Mark `svelte`/`svelte/*` as external; declare `svelte` in `peerDependencies`
- Use `svelte2tsx` to emit `.d.ts` for Svelte components
- Keep `.svelte` in source form for distribution
## Related
- [Plugins](advanced-plugins.md) - Plugin configuration
- [Dependencies](option-dependencies.md) - External dependencies
@@ -0,0 +1,387 @@
# Vue Support
Build Vue component libraries with tsdown.
## Overview
tsdown provides first-class support for Vue libraries through integration with `unplugin-vue` and `rolldown-plugin-dts` for type generation.
## Quick Start
### Use Starter Template
```bash
npx create-tsdown@latest -t vue
```
## Basic Configuration
### Install Dependencies
```bash
pnpm add -D unplugin-vue vue-tsc
```
### Minimal Setup
```ts
// tsdown.config.ts
import { defineConfig } from 'tsdown'
import Vue from 'unplugin-vue/rolldown'
export default defineConfig({
entry: ['./src/index.ts'],
format: ['esm', 'cjs'],
platform: 'neutral',
deps: {
neverBundle: ['vue'],
},
plugins: [
Vue({ isProduction: true }),
],
dts: {
vue: true, // Enable Vue type generation
},
})
```
## How It Works
### unplugin-vue
Compiles `.vue` single-file components:
- Transforms template to render functions
- Handles scoped styles
- Processes script setup
### vue-tsc
Generates TypeScript declarations:
- Type-checks Vue components
- Creates `.d.ts` files
- Preserves component props types
- Exports component types
## Component Example
### Single File Component
```vue
<!-- src/Button.vue -->
<script setup lang="ts">
interface Props {
type?: 'primary' | 'secondary'
disabled?: boolean
}
defineProps<Props>()
defineEmits<{
click: []
}>()
</script>
<template>
<button
:class="['btn', `btn-${type}`]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<style scoped>
.btn {
padding: 8px 16px;
border-radius: 4px;
}
.btn-primary {
background: blue;
color: white;
}
</style>
```
### Export Components
```ts
// src/index.ts
export { default as Button } from './Button.vue'
export { default as Input } from './Input.vue'
export { default as Modal } from './Modal.vue'
// Re-export types
export type { ButtonProps } from './Button.vue'
```
## Common Patterns
### Component Library
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
platform: 'neutral',
deps: {
neverBundle: ['vue'],
},
plugins: [
Vue({
isProduction: true,
style: {
trim: true,
},
}),
],
dts: {
vue: true,
},
clean: true,
})
```
### Multiple Components
```ts
export default defineConfig({
entry: {
index: 'src/index.ts',
Button: 'src/Button.vue',
Input: 'src/Input.vue',
Modal: 'src/Modal.vue',
},
format: ['esm', 'cjs'],
deps: {
neverBundle: ['vue'],
},
plugins: [Vue({ isProduction: true })],
dts: { vue: true },
})
```
### With Composition Utilities
```ts
// src/composables/useCounter.ts
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
```
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
deps: {
neverBundle: ['vue'],
},
plugins: [Vue({ isProduction: true })],
dts: { vue: true },
})
```
### TypeScript Configuration
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"strict": true,
"isolatedDeclarations": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
```
### Package.json Configuration
```json
{
"name": "my-vue-library",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
},
"files": ["dist"],
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"tsdown": "^0.9.0",
"typescript": "^5.0.0",
"unplugin-vue": "^5.0.0",
"vue": "^3.4.0",
"vue-tsc": "^2.0.0"
}
}
```
## Advanced Patterns
### With Vite Plugins
Some Vite Vue plugins may work:
```ts
import Vue from 'unplugin-vue/rolldown'
import Components from 'unplugin-vue-components/rolldown'
export default defineConfig({
entry: ['src/index.ts'],
deps: {
neverBundle: ['vue'],
},
plugins: [
Vue({ isProduction: true }),
Components({
dts: 'src/components.d.ts',
}),
],
dts: { vue: true },
})
```
### JSX Support
```ts
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
deps: {
neverBundle: ['vue'],
},
plugins: [
Vue({
isProduction: true,
script: {
propsDestructure: true,
},
}),
],
inputOptions: {
transform: {
jsx: 'automatic',
jsxImportSource: 'vue',
},
},
dts: { vue: true },
})
```
### Monorepo Vue Packages
```ts
export default defineConfig({
workspace: 'packages/*',
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
deps: {
neverBundle: ['vue', /^@mycompany\//],
},
plugins: [Vue({ isProduction: true })],
dts: { vue: true },
})
```
## Plugin Options
### unplugin-vue Options
```ts
Vue({
isProduction: true,
script: {
defineModel: true,
propsDestructure: true,
},
style: {
trim: true,
},
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('custom-'),
},
},
})
```
## Tips
1. **Always externalize Vue** - Don't bundle Vue itself
2. **Enable vue: true in dts** - For proper type generation
3. **Use platform: 'neutral'** - Maximum compatibility
4. **Install vue-tsc** - Required for type generation
5. **Set isProduction: true** - Optimize for production
6. **Add peer dependency** - Vue as peer dependency
## Troubleshooting
### Type Generation Fails
Ensure vue-tsc is installed:
```bash
pnpm add -D vue-tsc
```
Enable in config:
```ts
dts: { vue: true }
```
### Component Types Missing
Check TypeScript config:
```json
{
"compilerOptions": {
"jsx": "preserve",
"moduleResolution": "bundler"
}
}
```
### Vue Not Externalized
Add to deps.neverBundle:
```ts
deps: {
neverBundle: ['vue'],
}
```
### SFC Compilation Errors
Check unplugin-vue version:
```bash
pnpm add -D unplugin-vue@latest
```
## Related
- [Plugins](advanced-plugins.md) - Plugin system
- [Dependencies](option-dependencies.md) - External packages
- [DTS](option-dts.md) - Type declarations
- [React Recipe](recipe-react.md) - React component libraries
@@ -0,0 +1,123 @@
# WASM Support
Bundle WebAssembly modules in your TypeScript/JavaScript project.
## Overview
tsdown supports WASM through [`rolldown-plugin-wasm`](https://github.com/sxzz/rolldown-plugin-wasm), enabling direct `.wasm` imports with synchronous and asynchronous instantiation.
## Setup
### Install
```bash
pnpm add -D rolldown-plugin-wasm
```
### Configure
```ts
import { wasm } from 'rolldown-plugin-wasm'
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['./src/index.ts'],
plugins: [wasm()],
})
```
### TypeScript Support
Add type declarations to `tsconfig.json`:
```jsonc
{
"compilerOptions": {
"types": ["rolldown-plugin-wasm/types"]
}
}
```
## Importing WASM Modules
### Direct Import
```ts
import { add } from './add.wasm'
add(1, 2)
```
### Async Init
Use `?init` query for async initialization:
```ts
import init from './add.wasm?init'
const instance = await init(imports) // imports optional
instance.exports.add(1, 2)
```
### Sync Init
Use `?init&sync` query for synchronous initialization:
```ts
import initSync from './add.wasm?init&sync'
const instance = initSync(imports) // imports optional
instance.exports.add(1, 2)
```
## wasm-bindgen Support
### Target `bundler` (Recommended)
```ts
import { add } from 'some-pkg'
add(1, 2)
```
### Target `web` (Node.js)
```ts
import { readFile } from 'node:fs/promises'
import init, { add } from 'some-pkg'
import wasmUrl from 'some-pkg/add_bg.wasm?url'
await init({
module_or_path: readFile(new URL(wasmUrl, import.meta.url)),
})
add(1, 2)
```
### Target `web` (Browser)
```ts
import init, { add } from 'some-pkg/add.js'
import wasmUrl from 'some-pkg/add_bg.wasm?url'
await init({ module_or_path: wasmUrl })
add(1, 2)
```
`nodejs` and `no-modules` wasm-bindgen targets are not supported.
## Plugin Options
```ts
wasm({
maxFileSize: 14 * 1024, // Max size for inline (default: 14KB)
fileName: '[hash][extname]', // Output file name pattern
targetEnv: 'auto', // 'auto' | 'auto-inline' | 'browser' | 'node'
})
```
| Option | Default | Description |
|--------|---------|-------------|
| `maxFileSize` | `14 * 1024` | Max file size for inlining. Set to `0` to always copy. |
| `fileName` | `'[hash][extname]'` | Pattern for emitted WASM files |
| `targetEnv` | `'auto'` | `'auto'` detects at runtime; `'browser'` omits Node builtins; `'node'` omits fetch |
## Related Options
- [Plugins](advanced-plugins.md) - Plugin system overview
- [Platform](option-platform.md) - Target platform configuration
@@ -0,0 +1,472 @@
# CLI Reference
Complete reference for tsdown command-line interface.
## Overview
All CLI flags can also be set in the config file. CLI flags override config file options.
## Flag Patterns
CLI flag mapping rules:
- `--foo` sets `foo: true`
- `--no-foo` sets `foo: false`
- `--foo.bar` sets `foo: { bar: true }`
- `--format esm --format cjs` sets `format: ['esm', 'cjs']`
CLI flags support both camelCase and kebab-case. For example, `--outDir` and `--out-dir` are equivalent.
## Basic Commands
### Build
```bash
# Build with default config
tsdown
# Build specific files
tsdown src/index.ts src/cli.ts
# Build with watch mode
tsdown --watch
```
## Configuration
### `--config, -c <filename>`
Specify custom config file:
```bash
tsdown --config build.config.ts
tsdown -c custom-config.js
```
### `--no-config`
Disable config file loading:
```bash
tsdown --no-config src/index.ts
```
### `--config-loader <loader>`
Choose config loader (`auto`, `native`, `unrun`):
```bash
tsdown --config-loader unrun
```
### `--tsconfig <file>`
Specify TypeScript config file:
```bash
tsdown --tsconfig tsconfig.build.json
```
## Entry Points
### `[...files]`
Specify entry files as arguments:
```bash
tsdown src/index.ts src/utils.ts
```
## Output Options
### `--format <format>`
Output format (`esm`, `cjs`, `iife`, `umd`):
```bash
tsdown --format esm
tsdown --format esm --format cjs
```
### `--out-dir, -d <dir>`
Output directory:
```bash
tsdown --out-dir lib
tsdown -d dist
```
### `--dts`
Generate TypeScript declarations:
```bash
tsdown --dts
```
### `--clean`
Clean output directory before build:
```bash
tsdown --clean
```
## Build Options
### `--target <target>`
JavaScript target version:
```bash
tsdown --target es2020
tsdown --target node18
tsdown --target chrome100
tsdown --no-target # Disable transformations
```
### `--platform <platform>`
Target platform (`node`, `browser`, `neutral`):
```bash
tsdown --platform node
tsdown --platform browser
```
### `--minify`
Enable minification:
```bash
tsdown --minify
tsdown --no-minify
```
### `--sourcemap`
Generate source maps:
```bash
tsdown --sourcemap
tsdown --sourcemap inline
```
### `--treeshake`
Enable/disable tree shaking:
```bash
tsdown --treeshake
tsdown --no-treeshake
```
## Dependencies
### `--deps.never-bundle <module>`
Mark module as external (not bundled):
```bash
tsdown --deps.never-bundle react --deps.never-bundle react-dom
```
### `--deps.skip-node-modules-bundle`
Skip resolving and bundling all node_modules:
```bash
tsdown --deps.skip-node-modules-bundle
```
### `--shims`
Add ESM/CJS compatibility shims:
```bash
tsdown --shims
```
## Development
### `--watch, -w [path]`
Enable watch mode:
```bash
tsdown --watch
tsdown -w
tsdown --watch src # Watch specific directory
```
### `--ignore-watch <path>`
Ignore paths in watch mode:
```bash
tsdown --watch --ignore-watch test
```
### `--on-success <command>`
Run command after successful build:
```bash
tsdown --watch --on-success "echo Build complete!"
```
## Environment Variables
### `--env.* <value>`
Set compile-time environment variables:
```bash
tsdown --env.NODE_ENV=production --env.API_URL=https://api.example.com
```
Access as `import.meta.env.*` or `process.env.*`.
### `--env-file <file>`
Load environment variables from file:
```bash
tsdown --env-file .env.production
```
### `--env-prefix <prefix>`
Filter environment variables by prefix (default: `TSDOWN_`):
```bash
tsdown --env-file .env --env-prefix APP_ --env-prefix TSDOWN_
```
## Assets
### `--copy <dir>`
Copy directory to output:
```bash
tsdown --copy public
tsdown --copy assets --copy static
```
## Executable
### `--exe`
**[experimental]** Bundle as a standalone executable using [Node.js Single Executable Applications](https://nodejs.org/api/single-executable-applications.html). Requires Node.js >= 25.5.0, not supported in Bun or Deno. Cross-platform builds supported via `@tsdown/exe`.
```bash
tsdown --exe
```
When enabled:
- Default format changes to `cjs` (unless Node.js >= 25.7.0)
- Declaration file generation (`dts`) is disabled by default
- Code splitting is disabled
- Only single entry points are supported
See [Executable](option-exe.md) for advanced configuration and cross-platform builds.
## Package Management
### `--exports`
Generate the `exports` field in package.json:
```bash
tsdown --exports
```
### `--publint`
Enable package validation:
```bash
tsdown --publint
```
### `--attw`
Enable "Are the types wrong" validation:
```bash
tsdown --attw
```
### `--unused`
Check for unused dependencies:
```bash
tsdown --unused
```
## Logging
### `--log-level <level>`
Set logging verbosity (`silent`, `error`, `warn`, `info`):
```bash
tsdown --log-level error
tsdown --log-level warn
```
### `--report` / `--no-report`
Enable/disable build report:
```bash
tsdown --no-report # Disable size report
tsdown --report # Enable (default)
```
### `--debug [feat]`
Show debug logs:
```bash
tsdown --debug
tsdown --debug rolldown # Debug specific feature
```
## Integration
### `--from-vite [vitest]`
Extend Vite or Vitest config:
```bash
tsdown --from-vite # Use vite.config.*
tsdown --from-vite vitest # Use vitest.config.*
```
## Workspace / Monorepo
### `--workspace, -W [dir]`
Enable workspace mode for building multiple packages:
```bash
tsdown -W
tsdown -W packages/
```
### `--filter, -F <pattern>`
Filter configs by name or working directory. Supports regex:
```bash
tsdown -W -F my-package
tsdown -W -F /^pkg-/
```
### `--unbundle`
Enable unbundle (bundleless) mode:
```bash
tsdown --unbundle
```
### `--root <dir>`
Specify the root directory of input files (similar to TypeScript's `rootDir`). Controls the output directory structure by determining how entry file paths map to output paths. Defaults to the common base directory of all entry files.
```bash
tsdown --root src
tsdown --root .
```
### `--fail-on-warn`
Fail on warnings (enabled by default):
```bash
tsdown --no-fail-on-warn # Disable
```
## Common Usage Patterns
### Basic Build
```bash
tsdown
```
### Library (ESM + CJS + Types)
```bash
tsdown --format esm --format cjs --dts --clean
```
### Production Build
```bash
tsdown --minify --clean --no-report
```
### Development (Watch)
```bash
tsdown --watch --sourcemap
```
### Browser Bundle (IIFE)
```bash
tsdown --format iife --platform browser --minify
```
### Node.js CLI Tool
```bash
tsdown --format esm --platform node --shims
```
### Standalone Executable
```bash
tsdown src/cli.ts --exe
```
### Monorepo Package
```bash
tsdown --clean --dts --exports --publint
```
### With Environment Variables
```bash
tsdown --env-file .env.production --env.BUILD_TIME=$(date +%s)
```
### Copy Assets
```bash
tsdown --copy public --copy assets --clean
```
## Tips
1. **Use config file** for complex setups
2. **CLI flags override** config file options
3. **Chain multiple formats** for multi-target builds
4. **Use --clean** to avoid stale files
5. **Enable --dts** for TypeScript libraries
6. **Use --watch** during development
7. **Add --on-success** for post-build tasks
8. **Use --exports** to auto-generate package.json fields
## Related Documentation
- [Config File](option-config-file.md) - Configuration file options
- [Entry](option-entry.md) - Entry point configuration
- [Output Format](option-output-format.md) - Format options
- [Watch Mode](option-watch-mode.md) - Watch mode details
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 hyf0, SerKo <https://github.com/serkodev>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,29 @@
---
name: vue-testing-best-practices
version: 1.0.0
license: MIT
author: github.com/vuejs-ai
description: Use for Vue.js testing. Covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing.
---
Vue.js testing best practices, patterns, and common gotchas.
### Testing
- Setting up test infrastructure for Vue 3 projects → See [testing-vitest-recommended-for-vue](reference/testing-vitest-recommended-for-vue.md)
- Tests keep breaking when refactoring component internals → See [testing-component-blackbox-approach](reference/testing-component-blackbox-approach.md)
- Tests fail intermittently with race conditions → See [testing-async-await-flushpromises](reference/testing-async-await-flushpromises.md)
- Composables using lifecycle hooks or inject fail to test → See [testing-composables-helper-wrapper](reference/testing-composables-helper-wrapper.md)
- Getting "injection Symbol(pinia) not found" errors in tests → See [testing-pinia-store-setup](reference/testing-pinia-store-setup.md)
- Components with async setup won't render in tests → See [testing-suspense-async-components](reference/testing-suspense-async-components.md)
- Snapshot tests keep passing despite broken functionality → See [testing-no-snapshot-only](reference/testing-no-snapshot-only.md)
- Choosing end-to-end testing framework for Vue apps → See [testing-e2e-playwright-recommended](reference/testing-e2e-playwright-recommended.md)
- Tests need to verify computed styles or real DOM events → See [testing-browser-vs-node-runners](reference/testing-browser-vs-node-runners.md)
- Testing components created with defineAsyncComponent fails → See [async-component-testing](reference/async-component-testing.md)
- Teleported modal content can't be found in wrapper queries → See [teleport-testing-complexity](reference/teleport-testing-complexity.md)
## Reference
- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)
- [Vue Test Utils](https://test-utils.vuejs.org/)
- [Vitest Documentation](https://vitest.dev/)
- [Playwright Documentation](https://playwright.dev/)
@@ -0,0 +1,5 @@
# Sync Info
- **Source:** `vendor/vuejs-ai/skills/vue-testing-best-practices`
- **Git SHA:** `f3dd1bf4d3ac78331bdc903e4519d561c538ca6a`
- **Synced:** 2026-03-16
@@ -0,0 +1,163 @@
---
title: Use flushPromises for Testing Async Components
impact: HIGH
impactDescription: Without awaiting async operations, tests make assertions before the component has rendered, causing false negatives
type: gotcha
tags: [vue3, testing, async, defineAsyncComponent, flushPromises, vitest]
---
# Use flushPromises for Testing Async Components
**Impact: HIGH** - When testing async components created with `defineAsyncComponent`, you must use `await flushPromises()` to ensure the component has loaded before making assertions. Vue updates asynchronously, so tests that don't account for this will make assertions before the component has rendered.
## Task Checklist
- [ ] Use `async/await` in test functions for async components
- [ ] Call `await flushPromises()` after mounting async components
- [ ] Test loading states by making assertions before `flushPromises()`
- [ ] Test error states using rejected promises in `defineAsyncComponent`
- [ ] Use `trigger()` with `await` as it returns a Promise
**Incorrect:**
```javascript
import { mount } from '@vue/test-utils'
import { defineAsyncComponent } from 'vue'
const AsyncWidget = defineAsyncComponent(() =>
import('./Widget.vue')
)
test('renders async component', () => {
const wrapper = mount(AsyncWidget)
// FAILS: Component hasn't loaded yet
expect(wrapper.text()).toContain('Widget Content')
})
```
**Correct:**
```javascript
import { mount, flushPromises } from '@vue/test-utils'
import { defineAsyncComponent, nextTick } from 'vue'
const AsyncWidget = defineAsyncComponent(() =>
import('./Widget.vue')
)
test('renders async component', async () => {
const wrapper = mount(AsyncWidget)
// Wait for async component to load
await flushPromises()
expect(wrapper.text()).toContain('Widget Content')
})
test('shows loading state initially', async () => {
const AsyncWithLoading = defineAsyncComponent({
loader: () => import('./Widget.vue'),
loadingComponent: { template: '<div>Loading...</div>' },
delay: 0
})
const wrapper = mount(AsyncWithLoading)
// Check loading state immediately
expect(wrapper.text()).toContain('Loading...')
// Wait for component to load
await flushPromises()
// Check final state
expect(wrapper.text()).toContain('Widget Content')
})
```
## Testing with Suspense
```javascript
import { mount, flushPromises } from '@vue/test-utils'
import { Suspense, defineAsyncComponent, h } from 'vue'
const AsyncWidget = defineAsyncComponent(() =>
import('./Widget.vue')
)
test('renders async component with Suspense', async () => {
const wrapper = mount({
components: { AsyncWidget },
template: `
<Suspense>
<AsyncWidget />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
`
})
// Initially shows fallback
expect(wrapper.text()).toContain('Loading...')
// Wait for async resolution
await flushPromises()
// Now shows actual content
expect(wrapper.text()).toContain('Widget Content')
})
```
## Testing Error States
```javascript
import { mount, flushPromises } from '@vue/test-utils'
import { defineAsyncComponent } from 'vue'
test('shows error component on load failure', async () => {
const AsyncWithError = defineAsyncComponent({
loader: () => Promise.reject(new Error('Failed to load')),
errorComponent: { template: '<div>Error loading component</div>' }
})
const wrapper = mount(AsyncWithError)
await flushPromises()
expect(wrapper.text()).toContain('Error loading component')
})
```
## Utilities Reference
| Utility | Purpose |
|---------|---------|
| `await flushPromises()` | Resolves all pending promises |
| `await nextTick()` | Waits for Vue's next DOM update cycle |
| `await wrapper.trigger('click')` | Triggers event and waits for update |
## Dynamic Import Handling
**Note:** Dynamic imports (`import('./File.vue')`) may require additional handling beyond `flushPromises()` in test environments. Test runners like Vitest handle module resolution differently than runtime bundlers, which can cause timing issues with dynamic imports. If `flushPromises()` alone doesn't resolve the component, consider:
- Mocking the dynamic import to return the component synchronously
- Using multiple `await flushPromises()` calls in sequence
- Wrapping assertions in `waitFor()` or retry utilities
- Configuring your test runner's module resolution settings
```javascript
// If flushPromises() isn't sufficient, mock the import
vi.mock('./Widget.vue', () => ({
default: { template: '<div>Widget Content</div>' }
}))
// Or use multiple flush calls for nested async operations
await flushPromises()
await flushPromises()
```
## References
- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense)
- [Vue.js Async Components Documentation](https://vuejs.org/guide/components/async)
@@ -0,0 +1,158 @@
---
title: Teleported Content Requires Special Testing Approach
impact: MEDIUM
impactDescription: Vue Test Utils cannot find teleported content using standard wrapper.find() methods
type: gotcha
tags: [vue3, teleport, testing, vue-test-utils]
---
# Teleported Content Requires Special Testing Approach
**Impact: MEDIUM** - Vue Test Utils scopes queries to the mounted component. Teleported content renders outside the component's DOM tree, so `wrapper.find()` cannot locate it. This leads to failing tests and confusion.
## Task Checklist
- [ ] Stub Teleport in unit tests to keep content in component tree
- [ ] Use `document.body` queries for integration tests with real Teleport
- [ ] Consider using `getComponent()` instead of DOM queries for teleported components
**Problem - Standard Testing Fails:**
```vue
<!-- Modal.vue -->
<template>
<button @click="open = true">Open</button>
<Teleport to="body">
<div v-if="open" class="modal" data-testid="modal">
<input type="text" data-testid="modal-input" />
</div>
</Teleport>
</template>
```
```ts
// Modal.spec.ts - BROKEN
import { mount } from '@vue/test-utils'
import Modal from './Modal.vue'
test('modal input exists', async () => {
const wrapper = mount(Modal)
await wrapper.find('button').trigger('click')
// FAILS: Teleported content is not in wrapper's DOM tree
expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true)
})
```
**Solution 1 - Stub Teleport:**
```ts
import { mount } from '@vue/test-utils'
import Modal from './Modal.vue'
test('modal input exists', async () => {
const wrapper = mount(Modal, {
global: {
stubs: {
// Stub teleport to render content inline
Teleport: true
}
}
})
await wrapper.find('button').trigger('click')
// Works: Content renders inside wrapper
expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true)
})
```
**Solution 2 - Query Document Body:**
```ts
import { mount } from '@vue/test-utils'
import Modal from './Modal.vue'
test('modal renders to body', async () => {
const wrapper = mount(Modal, {
attachTo: document.body // Required for Teleport to work
})
await wrapper.find('button').trigger('click')
// Query the actual DOM
const modal = document.querySelector('[data-testid="modal"]')
expect(modal).toBeTruthy()
const input = document.querySelector('[data-testid="modal-input"]')
expect(input).toBeTruthy()
// Cleanup
wrapper.unmount()
})
```
**Solution 3 - Custom Teleport Stub with Content Access:**
```ts
import { mount, config } from '@vue/test-utils'
import { h, Teleport } from 'vue'
import Modal from './Modal.vue'
// Custom stub that renders content in a testable way
const TeleportStub = {
setup(props, { slots }) {
return () => h('div', { class: 'teleport-stub' }, slots.default?.())
}
}
test('modal with custom stub', async () => {
const wrapper = mount(Modal, {
global: {
stubs: {
Teleport: TeleportStub
}
}
})
await wrapper.find('button').trigger('click')
// Content is inside .teleport-stub
expect(wrapper.find('.teleport-stub [data-testid="modal-input"]').exists()).toBe(true)
})
```
## Testing Vue Final Modal and UI Libraries
Libraries like Vue Final Modal use Teleport internally, causing test failures:
```ts
// Problem: Vue Final Modal teleports to body
import { VueFinalModal } from 'vue-final-modal'
test('modal content', async () => {
const wrapper = mount(MyComponent, {
global: {
stubs: {
// Stub the modal component to avoid teleport issues
VueFinalModal: true
}
}
})
})
```
## E2E Testing (Cypress, Playwright)
E2E tests query the real DOM, so Teleport works naturally:
```ts
// Cypress
it('opens modal', () => {
cy.visit('/page-with-modal')
cy.get('button').click()
// Works: Cypress queries the real DOM
cy.get('[data-testid="modal"]').should('be.visible')
})
```
## Reference
- [Vue Test Utils - Teleport](https://test-utils.vuejs.org/guide/advanced/teleport)
- [Vue Test Utils - Stubs](https://test-utils.vuejs.org/guide/advanced/stubs-shallow-mount)
@@ -0,0 +1,175 @@
---
title: Properly Handle Async Updates with nextTick and flushPromises
impact: HIGH
impactDescription: Race conditions and flaky tests occur when async DOM updates or API calls complete after assertions run
type: gotcha
tags: [vue3, testing, async, flushPromises, nextTick, vitest, vue-test-utils, race-condition]
---
# Properly Handle Async Updates with nextTick and flushPromises
**Impact: HIGH** - Vue updates the DOM asynchronously. Without properly awaiting these updates, tests may assert against stale DOM state, causing intermittent failures and false negatives.
Use `await` with triggers and `setValue`, use `nextTick` for reactive updates, and use `flushPromises` for external async operations like API calls.
## Task Checklist
- [ ] Always await `trigger()` and `setValue()` calls
- [ ] Use `await nextTick()` after programmatic reactive state changes
- [ ] Use `await flushPromises()` for external async operations (API calls, timers)
- [ ] Don't chain multiple `nextTick` calls - use `flushPromises` instead
- [ ] Consider using `waitFor` from testing-library for polling assertions
**Incorrect:**
```javascript
import { mount } from '@vue/test-utils'
import SearchComponent from './SearchComponent.vue'
// BAD: Not awaiting trigger - assertion runs before DOM updates
test('search filters results', () => {
const wrapper = mount(SearchComponent)
wrapper.find('input').setValue('vue') // Missing await!
wrapper.find('button').trigger('click') // Missing await!
// This assertion likely fails - DOM hasn't updated yet
expect(wrapper.findAll('.result').length).toBe(3)
})
// BAD: Using nextTick for API calls
test('loads data from API', async () => {
const wrapper = mount(DataLoader)
await nextTick() // This won't wait for the API call!
// Assertion runs before fetch completes
expect(wrapper.find('.data').text()).toBe('Loaded data')
})
```
**Correct:**
```javascript
import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import SearchComponent from './SearchComponent.vue'
import DataLoader from './DataLoader.vue'
// CORRECT: Await trigger and setValue
test('search filters results', async () => {
const wrapper = mount(SearchComponent)
await wrapper.find('input').setValue('vue')
await wrapper.find('button').trigger('click')
expect(wrapper.findAll('.result').length).toBe(3)
})
// CORRECT: Use flushPromises for API calls
test('loads data from API', async () => {
const wrapper = mount(DataLoader)
// Wait for all pending promises to resolve
await flushPromises()
expect(wrapper.find('.data').text()).toBe('Loaded data')
})
```
## When to Use Each Method
### `await trigger()` / `await setValue()` - User Interactions
```javascript
// These methods return nextTick internally
await wrapper.find('button').trigger('click')
await wrapper.find('input').setValue('new value')
await wrapper.find('form').trigger('submit')
```
### `await nextTick()` - Programmatic Reactive Updates
```javascript
import { nextTick } from 'vue'
test('reflects programmatic state changes', async () => {
const wrapper = mount(Counter)
// Direct state modification (when testing with exposed internals)
wrapper.vm.count = 5
await nextTick() // Wait for Vue to update DOM
expect(wrapper.find('.count').text()).toBe('5')
})
```
### `await flushPromises()` - External Async Operations
```javascript
import { flushPromises } from '@vue/test-utils'
test('displays fetched data', async () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
})
// Wait for component's API call to complete
await flushPromises()
expect(wrapper.find('.username').text()).toBe('John')
})
// Sometimes you need multiple flushPromises for chained async operations
test('processes data after fetch', async () => {
const wrapper = mount(DataProcessor)
await flushPromises() // Wait for fetch
await flushPromises() // Wait for processing triggered by fetch
expect(wrapper.find('.processed').exists()).toBe(true)
})
```
## Common Pattern: Combining Methods
```javascript
test('submits form and shows success', async () => {
const wrapper = mount(ContactForm)
// Fill form (awaiting each interaction)
await wrapper.find('#name').setValue('John')
await wrapper.find('#email').setValue('john@example.com')
// Submit form
await wrapper.find('form').trigger('submit')
// Wait for API submission to complete
await flushPromises()
// Assert success state
expect(wrapper.find('.success-message').exists()).toBe(true)
})
```
## Testing with MSW or Mock APIs
```javascript
import { flushPromises } from '@vue/test-utils'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ name: 'John' }))
})
)
test('displays user data', async () => {
const wrapper = mount(UserCard)
// MSW might require multiple flushPromises
await flushPromises()
await flushPromises()
expect(wrapper.find('.name').text()).toBe('John')
})
```
## Reference
- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense)
- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)
@@ -0,0 +1,208 @@
---
title: Choose Browser-Based Runner for Style and DOM Event Testing
impact: MEDIUM
impactDescription: Node-based runners cannot test real CSS behavior, native DOM events, cookies, or computed styles
type: capability
tags: [vue3, testing, component-testing, vitest, browser, jsdom]
---
# Choose Browser-Based Runner for Style and DOM Event Testing
**Impact: MEDIUM** - Node-based test runners (Vitest with jsdom/happy-dom) simulate the DOM but cannot test real CSS rendering, native browser events, cookies, computed styles, or cross-browser behavior. Use browser-based runners when these matter.
Use Vitest for most component tests (fast), but use Vitest Browser Mode when testing visual/DOM-dependent features.
## Task Checklist
- [ ] Use Vitest (node) for logic-focused component tests
- [ ] Use Vitest Browser Mode for style-dependent tests
- [ ] Use Vitest Browser Mode for native events (focus, drag, resize)
- [ ] Use Vitest Browser Mode for cookies and computed CSS styles
- [ ] Accept slower speed tradeoff for browser accuracy
## When to Use Each Approach
### Node-Based Runner (Vitest + happy-dom/jsdom)
Best for:
- Pure logic testing
- State management
- Event emission
- Props/slots behavior
- Most component interactions
- Fast CI/CD pipelines
```javascript
// vitest.config.js
export default defineConfig({
test: {
environment: 'happy-dom', // or 'jsdom'
}
})
```
```javascript
// Fast but limited - fine for most tests
test('button emits click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
```
### Vitest Browser Mode
Required for:
- CSS computed styles verification
- CSS transitions/animations
- Real focus/blur behavior
- Drag and drop
- Cookie operations
- Viewport-dependent behavior
- Cross-browser validation
## Vitest Browser Mode Setup
```bash
npm install -D @vitest/browser playwright
```
```javascript
// vitest.config.js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
},
},
})
```
```javascript
// Button.browser.test.js
import { render } from 'vitest-browser-vue'
import Button from './Button.vue'
test('has correct hover styling', async () => {
const { getByRole } = render(Button, { props: { label: 'Click me' } })
const button = getByRole('button')
// Check initial style
await expect.element(button).toHaveStyle({
backgroundColor: 'rgb(59, 130, 246)' // blue
})
})
test('maintains focus after click', async () => {
const { getByRole } = render(Button)
const button = getByRole('button')
await button.click()
await expect.element(button).toHaveFocus()
})
```
## Examples: What Each Runner Can/Cannot Test
### Styles - Browser Required
```javascript
// Node runner: CANNOT verify actual CSS
test('danger button has red background', () => {
const wrapper = mount(Button, { props: { variant: 'danger' } })
// This only checks class exists, not actual color
expect(wrapper.classes()).toContain('bg-red-500')
})
// Vitest Browser Mode: CAN verify computed styles
test('danger button renders red', async () => {
const { getByRole } = render(Button, { props: { variant: 'danger' } })
await expect.element(getByRole('button')).toHaveStyle({
backgroundColor: 'rgb(239, 68, 68)'
})
})
```
### Computed CSS Styles - Browser Required
```javascript
// Node runner: CANNOT get real computed styles
test('button has correct padding', () => {
const wrapper = mount(Button)
// getComputedStyle returns empty/default values in jsdom
const style = window.getComputedStyle(wrapper.element)
// style.padding will be empty string, not actual computed value
})
// Vitest Browser Mode: Real computed styles
test('button has correct padding', async () => {
const { getByRole } = render(Button)
const button = getByRole('button')
await expect.element(button).toHaveStyle({
padding: '12px 24px'
})
})
```
### Native Events - Browser Required
```javascript
// Node runner: Synthetic events only
test('handles drag and drop', async () => {
const wrapper = mount(DraggableList)
// trigger('dragstart') is synthetic - may not work as expected
await wrapper.find('.item').trigger('dragstart')
})
// Vitest Browser Mode: Real native events via userEvent
import { userEvent } from '@vitest/browser/context'
test('reorders items on drag', async () => {
const { getByTestId } = render(DraggableList)
const item = getByTestId('item-1')
const target = getByTestId('item-3')
await userEvent.dragAndDrop(item, target)
// Assert reordering
})
```
## Recommended Testing Strategy
```javascript
// vitest.config.js - Separate test configurations
export default defineConfig({
test: {
// Default: Node environment for speed
environment: 'happy-dom',
// Browser tests in separate directory
include: ['src/**/*.test.{js,ts}'],
},
})
// Run browser tests separately
// npx vitest --browser.enabled
```
### Directory Structure
```
tests/
├── unit/ # Fast node-based tests
│ ├── Button.test.js
│ └── useCounter.test.js
├── component/ # Slower browser-based tests
│ ├── Button.browser.test.js
│ └── DragDrop.browser.test.js
└── e2e/ # Full E2E tests (Playwright)
└── user-flow.spec.ts
```
## Reference
- [Vue.js Testing - Component Testing](https://vuejs.org/guide/scaling-up/testing#component-testing)
- [Vitest Browser Mode](https://vitest.dev/guide/browser.html)
@@ -0,0 +1,144 @@
---
title: Test Components Using Blackbox Approach - Focus on Behavior Not Implementation
impact: HIGH
impactDescription: Implementation-aware tests become brittle and break during refactoring, leading to high maintenance burden
type: best-practice
tags: [vue3, testing, component-testing, vitest, vue-test-utils, blackbox]
---
# Test Components Using Blackbox Approach - Focus on Behavior Not Implementation
**Impact: HIGH** - Tests that rely on implementation details (internal state, private methods, component structure) break during refactoring even when functionality remains correct. This leads to false negatives and high test maintenance burden.
Follow Kent C. Dodds' testing philosophy: "The more your tests resemble how your software is used, the more confidence they can give you."
## Task Checklist
- [ ] Test what the component does, not how it does it
- [ ] Query elements by user-visible attributes (text, role, testid)
- [ ] Simulate user interactions (click, type) rather than calling methods directly
- [ ] Assert on rendered output, emitted events, and visible state changes
- [ ] Avoid accessing component internal state or private methods
- [ ] Use data-testid attributes for elements without semantic meaning
**Incorrect:**
```javascript
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
// BAD: Testing implementation details
test('counter increments', async () => {
const wrapper = mount(Counter)
// Accessing internal state directly
expect(wrapper.vm.count).toBe(0)
// Calling internal method instead of simulating user action
wrapper.vm.increment()
// Checking internal state instead of visible output
expect(wrapper.vm.count).toBe(1)
})
// BAD: Testing component structure
test('has increment button', () => {
const wrapper = mount(Counter)
// Testing implementation detail - what if button becomes an anchor?
expect(wrapper.find('button').exists()).toBe(true)
})
```
**Correct:**
```javascript
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
// CORRECT: Testing behavior like a user would
test('counter displays updated value after clicking increment', async () => {
const wrapper = mount(Counter, {
props: { max: 10 }
})
// Assert initial visible state
expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('0')
// Simulate user action
await wrapper.find('[data-testid="increment-button"]').trigger('click')
// Assert visible result
expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('1')
})
// CORRECT: Testing emitted events (public API)
test('emits change event with new value when incremented', async () => {
const wrapper = mount(Counter)
await wrapper.find('[data-testid="increment-button"]').trigger('click')
expect(wrapper.emitted('change')).toHaveLength(1)
expect(wrapper.emitted('change')[0]).toEqual([1])
})
```
## Using @testing-library/vue for Better Blackbox Tests
```javascript
import { render, screen, fireEvent } from '@testing-library/vue'
import Counter from './Counter.vue'
// Testing Library encourages accessible, user-centric queries
test('increments counter on button click', async () => {
render(Counter)
// Query by role - how screen readers see it
const button = screen.getByRole('button', { name: /increment/i })
const display = screen.getByText('0')
await fireEvent.click(button)
expect(screen.getByText('1')).toBeInTheDocument()
})
```
## What to Test vs What Not to Test
### DO Test (Public Interface)
```javascript
// Props affect rendered output
test('shows title from props', () => {
const wrapper = mount(Card, {
props: { title: 'Hello World' }
})
expect(wrapper.text()).toContain('Hello World')
})
// Slots render correctly
test('renders slot content', () => {
const wrapper = mount(Card, {
slots: { default: '<p>Slot content</p>' }
})
expect(wrapper.text()).toContain('Slot content')
})
// Emitted events
test('emits close event when X clicked', async () => {
const wrapper = mount(Modal)
await wrapper.find('[data-testid="close-button"]').trigger('click')
expect(wrapper.emitted('close')).toBeTruthy()
})
```
### DON'T Test (Implementation Details)
```javascript
// Don't test internal computed properties
// Don't test internal methods
// Don't test component options/setup internals
// Don't test that specific child components are rendered (unless critical)
// Don't rely exclusively on snapshot tests for correctness
```
## Reference
- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)
- [Vue Test Utils - Testing Philosophy](https://test-utils.vuejs.org/guide/)
- [Testing Library Guiding Principles](https://testing-library.com/docs/guiding-principles)
@@ -0,0 +1,238 @@
---
title: Test Complex Composables with Host Component Wrapper
impact: MEDIUM
impactDescription: Composables using lifecycle hooks or provide/inject fail when tested directly without a component context
type: capability
tags: [vue3, testing, composables, vitest, lifecycle-hooks, provide-inject]
---
# Test Complex Composables with Host Component Wrapper
**Impact: MEDIUM** - Composables that use Vue lifecycle hooks (`onMounted`, `onUnmounted`) or dependency injection (`inject`) require a component context to function. Testing them directly will cause errors or incorrect behavior.
Simple composables using only reactivity APIs can be tested directly. Complex composables need a helper function that creates a host component context.
## Task Checklist
- [ ] Identify if composable uses lifecycle hooks or inject
- [ ] For simple composables (refs, computed only): test directly
- [ ] For complex composables: use `withSetup` helper pattern
- [ ] Clean up by unmounting the test app after each test
- [ ] Use `app.provide()` to mock injected dependencies
**Simple Composable - Test Directly:**
```javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
const increment = () => count.value++
return { count, doubled, increment }
}
```
```javascript
// useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
// CORRECT: Simple composable can be tested directly
describe('useCounter', () => {
it('initializes with default value', () => {
const { count } = useCounter()
expect(count.value).toBe(0)
})
it('increments count', () => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
})
it('computes doubled value', () => {
const { count, doubled, increment } = useCounter(5)
expect(doubled.value).toBe(10)
increment()
expect(doubled.value).toBe(12)
})
})
```
**Complex Composable - Use Host Wrapper:**
```javascript
// composables/useFetch.js
import { ref, onMounted, onUnmounted, inject } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
let controller = null
// Uses inject - needs component context
const apiClient = inject('apiClient')
// Uses lifecycle hooks - needs component context
onMounted(async () => {
controller = new AbortController()
try {
const response = await apiClient.get(url, { signal: controller.signal })
data.value = response.data
} catch (e) {
if (e.name !== 'AbortError') error.value = e
} finally {
loading.value = false
}
})
onUnmounted(() => {
controller?.abort()
})
return { data, error, loading }
}
```
```javascript
// test-utils.js
import { createApp } from 'vue'
/**
* Helper to test composables that need component context
*/
export function withSetup(composable) {
let result
const app = createApp({
setup() {
result = composable()
// Return a render function to suppress warnings
return () => {}
}
})
app.mount(document.createElement('div'))
return [result, app]
}
```
```javascript
// useFetch.test.js
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { withSetup } from './test-utils'
import { useFetch } from './useFetch'
describe('useFetch', () => {
let app
const mockApiClient = {
get: vi.fn()
}
afterEach(() => {
// IMPORTANT: Clean up to trigger onUnmounted
app?.unmount()
})
it('fetches data on mount', async () => {
mockApiClient.get.mockResolvedValue({ data: { id: 1, name: 'Test' } })
const [result, testApp] = withSetup(() => useFetch('/api/test'))
app = testApp
// Provide mocked dependency
app.provide('apiClient', mockApiClient)
// Wait for async operations
await flushPromises()
expect(result.data.value).toEqual({ id: 1, name: 'Test' })
expect(result.loading.value).toBe(false)
expect(result.error.value).toBeNull()
})
it('handles errors', async () => {
const testError = new Error('Network error')
mockApiClient.get.mockRejectedValue(testError)
const [result, testApp] = withSetup(() => useFetch('/api/test'))
app = testApp
app.provide('apiClient', mockApiClient)
await flushPromises()
expect(result.error.value).toBe(testError)
expect(result.data.value).toBeNull()
})
})
```
## Enhanced withSetup Helper with Provide Support
```javascript
// test-utils.js
export function withSetup(composable, options = {}) {
let result
const app = createApp({
setup() {
result = composable()
return () => {}
}
})
// Apply global provides before mounting
if (options.provide) {
Object.entries(options.provide).forEach(([key, value]) => {
app.provide(key, value)
})
}
app.mount(document.createElement('div'))
return [result, app]
}
// Usage
const [result, app] = withSetup(() => useMyComposable(), {
provide: {
apiClient: mockApiClient,
currentUser: { id: 1, name: 'Test User' }
}
})
```
## Testing with @vue/test-utils mount
```javascript
import { mount } from '@vue/test-utils'
import { defineComponent } from 'vue'
import { useFetch } from './useFetch'
test('useFetch in component context', async () => {
const TestComponent = defineComponent({
setup() {
const { data, loading } = useFetch('/api/users')
return { data, loading }
},
template: '<div>{{ loading ? "Loading..." : data }}</div>'
})
const wrapper = mount(TestComponent, {
global: {
provide: {
apiClient: mockApiClient
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('Test data')
})
```
## Reference
- [Vue.js Testing Guide - Testing Composables](https://vuejs.org/guide/scaling-up/testing#testing-composables)
- [Vue Test Utils - Mounting Components](https://test-utils.vuejs.org/guide/)
@@ -0,0 +1,242 @@
---
title: Use Playwright for E2E Testing - Cross-Browser Support and Better DX
impact: MEDIUM
impactDescription: Cypress has browser limitations and some features require paid subscriptions
type: best-practice
tags: [vue3, testing, e2e, playwright, cypress, end-to-end]
---
# Use Playwright for E2E Testing - Cross-Browser Support and Better DX
**Impact: MEDIUM** - Playwright offers superior cross-browser testing (Chromium, WebKit, Firefox), excellent debugging tools, and is fully open source. Cypress has limitations with WebKit support and requires paid subscriptions for some features.
Use Playwright for new E2E testing setups. Consider Cypress if team already has expertise or for its visual debugging UI.
## Task Checklist
- [ ] Install Playwright with browsers for your target platforms
- [ ] Configure for Vue dev server integration
- [ ] Set up projects for different browsers
- [ ] Use locator strategies that match component test patterns
- [ ] Configure CI for parallel test execution
- [ ] Use trace and screenshot features for debugging
## Quick Setup
```bash
# Install Playwright
npm init playwright@latest
# This will create:
# - playwright.config.ts
# - tests/ directory
# - tests-examples/ directory
```
**playwright.config.ts:**
```typescript
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
// Base URL for navigation
baseURL: 'http://localhost:5173',
// Capture trace on first retry
trace: 'on-first-retry',
// Screenshot on failure
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile viewports
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
// Run local dev server before tests
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
})
```
## E2E Test Example
```typescript
// e2e/user-flow.spec.ts
import { test, expect } from '@playwright/test'
test.describe('User Authentication', () => {
test('user can log in and see dashboard', async ({ page }) => {
// Navigate to login
await page.goto('/login')
// Fill login form
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign In' }).click()
// Verify redirect to dashboard
await expect(page).toHaveURL('/dashboard')
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible()
})
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('wrong@example.com')
await page.getByLabel('Password').fill('wrongpassword')
await page.getByRole('button', { name: 'Sign In' }).click()
await expect(page.getByRole('alert')).toContainText('Invalid credentials')
await expect(page).toHaveURL('/login')
})
})
```
## Playwright vs Cypress Comparison
| Feature | Playwright | Cypress |
|---------|------------|---------|
| Browsers | Chromium, Firefox, WebKit | Chromium, Firefox, Electron (WebKit experimental) |
| Cross-browser | Full support | Limited |
| Parallelization | Built-in | Requires Cypress Cloud |
| Open source | Fully | Core only |
| Mobile testing | Device emulation | Limited |
| Debugging | Inspector, trace viewer | Time-travel UI |
| API testing | Built-in | Plugin required |
| Iframes | Full support | Limited |
## Testing Vue Components with Data-Testid
```typescript
// e2e/product-list.spec.ts
import { test, expect } from '@playwright/test'
test('user can add product to cart', async ({ page }) => {
await page.goto('/products')
// Use data-testid for reliable selectors
await page.getByTestId('product-card').first().click()
// Verify product detail page
await expect(page.getByTestId('product-title')).toBeVisible()
// Add to cart
await page.getByTestId('add-to-cart-button').click()
// Verify cart updated
await expect(page.getByTestId('cart-count')).toHaveText('1')
})
```
## Page Object Pattern for Vue Apps
```typescript
// e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test'
export class LoginPage {
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
readonly errorMessage: Locator
constructor(page: Page) {
this.page = page
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.submitButton = page.getByRole('button', { name: 'Sign In' })
this.errorMessage = page.getByRole('alert')
}
async goto() {
await this.page.goto('/login')
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
```
```typescript
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/LoginPage'
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
await expect(page).toHaveURL('/dashboard')
})
```
## Visual Regression Testing
```typescript
test('homepage visual regression', async ({ page }) => {
await page.goto('/')
// Full page screenshot comparison
await expect(page).toHaveScreenshot('homepage.png')
// Element-specific screenshot
await expect(page.getByTestId('hero-section')).toHaveScreenshot('hero.png')
})
```
## Running Tests
```bash
# Run all tests
npx playwright test
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific file
npx playwright test e2e/auth.spec.ts
# Run in specific browser
npx playwright test --project=chromium
# Debug mode
npx playwright test --debug
# Generate test from actions
npx playwright codegen localhost:5173
```
## Reference
- [Playwright Documentation](https://playwright.dev/)
- [Vue.js E2E Testing Recommendations](https://vuejs.org/guide/scaling-up/testing#e2e-testing)
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
@@ -0,0 +1,197 @@
---
title: Avoid Snapshot-Only Tests - They Don't Prove Correctness
impact: MEDIUM
impactDescription: Snapshot tests verify structure but not functionality, leading to false confidence and brittle tests
type: best-practice
tags: [vue3, testing, snapshot, vitest, vue-test-utils, anti-pattern]
---
# Avoid Snapshot-Only Tests - They Don't Prove Correctness
**Impact: MEDIUM** - Snapshot tests only verify that HTML structure hasn't changed - they don't verify that the component works correctly. Relying exclusively on snapshots leads to false confidence and tests that break on any refactoring, even when functionality is preserved.
Use snapshots sparingly for regression detection. Prefer behavioral assertions that test what the component does.
## Task Checklist
- [ ] Don't use snapshots as the only assertion for component behavior
- [ ] Use snapshots for regression detection on stable UI components
- [ ] Always pair snapshots with behavioral assertions
- [ ] Keep snapshots small and focused (avoid full component snapshots)
- [ ] Review snapshot diffs carefully - don't blindly update
- [ ] Consider inline snapshots for small, critical structures
**Incorrect:**
```javascript
import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'
// BAD: Snapshot-only test proves nothing about functionality
test('UserCard renders correctly', () => {
const wrapper = mount(UserCard, {
props: { user: { name: 'John', email: 'john@example.com' } }
})
expect(wrapper.html()).toMatchSnapshot()
})
// This test passes even if:
// - The email isn't clickable
// - The avatar doesn't load
// - User actions are completely broken
// - Accessibility is broken
```
**Correct:**
```javascript
import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'
// CORRECT: Test actual behavior
test('UserCard displays user information', () => {
const wrapper = mount(UserCard, {
props: { user: { name: 'John', email: 'john@example.com' } }
})
expect(wrapper.find('[data-testid="user-name"]').text()).toBe('John')
expect(wrapper.find('[data-testid="user-email"]').text()).toBe('john@example.com')
})
test('UserCard email link is clickable', async () => {
const wrapper = mount(UserCard, {
props: { user: { name: 'John', email: 'john@example.com' } }
})
const emailLink = wrapper.find('a[href^="mailto:"]')
expect(emailLink.exists()).toBe(true)
expect(emailLink.attributes('href')).toBe('mailto:john@example.com')
})
test('UserCard emits select event when clicked', async () => {
const wrapper = mount(UserCard, {
props: { user: { id: 1, name: 'John' } }
})
await wrapper.trigger('click')
expect(wrapper.emitted('select')).toBeTruthy()
expect(wrapper.emitted('select')[0]).toEqual([{ id: 1, name: 'John' }])
})
```
## When Snapshots ARE Useful
### Regression Detection for Stable Components
```javascript
// ACCEPTABLE: Snapshot as additional check, not the only check
test('ErrorBoundary renders error message', () => {
const wrapper = mount(ErrorBoundary, {
props: { error: new Error('Something went wrong') }
})
// Primary assertions - verify behavior
expect(wrapper.find('.error-title').text()).toBe('Error')
expect(wrapper.find('.error-message').text()).toContain('Something went wrong')
// Secondary snapshot - catches unexpected structural changes
expect(wrapper.find('.error-container').html()).toMatchSnapshot()
})
```
### Inline Snapshots for Small Structures
```javascript
// ACCEPTABLE: Inline snapshot for small, critical structure
test('generates correct list markup', () => {
const wrapper = mount(ListItem, { props: { item: 'Test' } })
expect(wrapper.html()).toMatchInlineSnapshot(`
"<li class="list-item">Test</li>"
`)
})
```
### Complex SVG or Icon Output
```javascript
// ACCEPTABLE: Snapshot for complex generated content
test('renders correct chart SVG', () => {
const wrapper = mount(PieChart, {
props: { data: [30, 40, 30] }
})
// Verify key behavior
expect(wrapper.findAll('path').length).toBe(3)
// Snapshot for full SVG structure
expect(wrapper.find('svg').html()).toMatchSnapshot()
})
```
## Better Alternatives to Snapshots
### Test Specific Elements
```javascript
// Instead of snapshotting entire component
test('renders product with all required fields', () => {
const wrapper = mount(ProductCard, {
props: { product: { name: 'Widget', price: 9.99, inStock: true } }
})
expect(wrapper.find('.product-name').text()).toBe('Widget')
expect(wrapper.find('.product-price').text()).toContain('9.99')
expect(wrapper.find('.in-stock-badge').exists()).toBe(true)
})
```
### Test CSS Classes for Styling
```javascript
test('applies danger styling for errors', () => {
const wrapper = mount(Alert, {
props: { type: 'error', message: 'Failed!' }
})
expect(wrapper.classes()).toContain('alert-danger')
expect(wrapper.find('.alert-icon').classes()).toContain('icon-error')
})
```
### Use Testing Library Queries
```javascript
import { render, screen } from '@testing-library/vue'
test('form has accessible labels', () => {
render(LoginForm)
// Testing Library queries verify accessibility
expect(screen.getByLabelText('Email')).toBeInTheDocument()
expect(screen.getByLabelText('Password')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument()
})
```
## Snapshot Anti-Patterns
```javascript
// ANTI-PATTERN: Giant component snapshot
test('page renders', () => {
const wrapper = mount(EntirePageComponent)
expect(wrapper.html()).toMatchSnapshot() // 500+ lines of HTML
})
// ANTI-PATTERN: Snapshot with dynamic content
test('shows current date', () => {
const wrapper = mount(DateDisplay)
expect(wrapper.html()).toMatchSnapshot() // Fails every day!
})
// ANTI-PATTERN: Snapshot after every test
test('button works', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.html()).toMatchSnapshot() // Redundant
})
```
## Reference
- [Vue.js Testing Guide - What Not to Test](https://vuejs.org/guide/scaling-up/testing)
- [Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing)
- [Vitest Snapshot Testing](https://vitest.dev/guide/snapshot.html)
@@ -0,0 +1,228 @@
---
title: Configure Pinia Testing with createTestingPinia and setActivePinia
impact: HIGH
impactDescription: Missing Pinia configuration causes 'injection Symbol(pinia) not found' errors and failing tests
type: gotcha
tags: [vue3, testing, pinia, vitest, store, mocking, createTestingPinia]
---
# Configure Pinia Testing with createTestingPinia and setActivePinia
**Impact: HIGH** - Testing components or composables that use Pinia stores without proper configuration results in "[Vue warn]: injection Symbol(pinia) not found" errors. Tests will fail or behave unexpectedly.
Use `@pinia/testing` package with `createTestingPinia` for component tests and `setActivePinia(createPinia())` for unit testing stores directly.
## Task Checklist
- [ ] Install `@pinia/testing` as a dev dependency
- [ ] Use `createTestingPinia` in component tests with `global.plugins`
- [ ] Use `setActivePinia(createPinia())` in `beforeEach` for store unit tests
- [ ] Configure `createSpy: vi.fn` when NOT using `globals: true` in Vitest
- [ ] Initialize store inside each test to get fresh state
- [ ] Use `stubActions: false` when you need real action execution
**Incorrect:**
```javascript
import { mount } from '@vue/test-utils'
import UserProfile from './UserProfile.vue'
// BAD: Missing Pinia - causes injection error
test('displays user name', () => {
const wrapper = mount(UserProfile) // ERROR: injection "Symbol(pinia)" not found
expect(wrapper.text()).toContain('John')
})
```
```javascript
import { useUserStore } from '@/stores/user'
// BAD: No active Pinia instance
test('user store actions', () => {
const store = useUserStore() // ERROR: no active Pinia
store.login('john', 'password')
})
```
**Correct - Component Testing:**
```javascript
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { vi } from 'vitest'
import UserProfile from './UserProfile.vue'
import { useUserStore } from '@/stores/user'
// CORRECT: Provide testing pinia with stubbed actions
test('displays user name', () => {
const wrapper = mount(UserProfile, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn, // Required if not using globals: true
initialState: {
user: { name: 'John', email: 'john@example.com' }
}
})
]
}
})
expect(wrapper.text()).toContain('John')
})
// CORRECT: Test with stubbed actions (default behavior)
test('calls logout action', async () => {
const wrapper = mount(UserProfile, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })]
}
})
// Get store AFTER mounting with createTestingPinia
const store = useUserStore()
await wrapper.find('[data-testid="logout"]').trigger('click')
// Actions are stubbed and wrapped in spies
expect(store.logout).toHaveBeenCalled()
})
```
**Correct - Store Unit Testing:**
```javascript
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
describe('User Store', () => {
beforeEach(() => {
// Create fresh Pinia instance for each test
setActivePinia(createPinia())
})
it('initializes with empty user', () => {
const store = useUserStore()
expect(store.user).toBeNull()
expect(store.isLoggedIn).toBe(false)
})
it('updates user on login', async () => {
const store = useUserStore()
// Real action executes - not stubbed
await store.login('john', 'password')
expect(store.user).toEqual({ name: 'John' })
expect(store.isLoggedIn).toBe(true)
})
it('clears user on logout', () => {
const store = useUserStore()
store.user = { name: 'John' } // Set initial state
store.logout()
expect(store.user).toBeNull()
})
})
```
## Testing with Real Actions vs Stubbed Actions
```javascript
import { createTestingPinia } from '@pinia/testing'
// Stubbed actions (default) - for isolation
const wrapper = mount(Component, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
// stubActions: true (default) - actions are mocked
})
]
}
})
// Real actions - for integration testing
const wrapper = mount(Component, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
stubActions: false // Actions execute normally
})
]
}
})
```
## Mocking Specific Action Implementations
```javascript
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { vi } from 'vitest'
import { useCartStore } from '@/stores/cart'
test('handles checkout failure', async () => {
const wrapper = mount(Checkout, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })]
}
})
const cartStore = useCartStore()
// Mock specific action behavior
cartStore.checkout.mockRejectedValue(new Error('Payment failed'))
await wrapper.find('[data-testid="checkout"]').trigger('click')
await flushPromises()
expect(wrapper.find('.error').text()).toContain('Payment failed')
})
```
## Spying on Actions with vi.spyOn
```javascript
import { setActivePinia, createPinia } from 'pinia'
import { vi } from 'vitest'
import { useUserStore } from '@/stores/user'
test('tracks action calls', async () => {
setActivePinia(createPinia())
const store = useUserStore()
const loginSpy = vi.spyOn(store, 'login')
loginSpy.mockResolvedValue({ success: true })
await store.login('john', 'password')
expect(loginSpy).toHaveBeenCalledWith('john', 'password')
})
```
## Testing Store $subscribe
```javascript
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
test('subscription triggers on state change', () => {
setActivePinia(createPinia())
const store = useUserStore()
const callback = vi.fn()
store.$subscribe(callback)
store.user = { name: 'John' }
expect(callback).toHaveBeenCalled()
})
```
## Reference
- [Pinia Testing Guide](https://pinia.vuejs.org/cookbook/testing.html)
- [@pinia/testing Package](https://www.npmjs.com/package/@pinia/testing)
- [Vue Test Utils - Plugins](https://test-utils.vuejs.org/guide/advanced/plugins.html)
@@ -0,0 +1,229 @@
---
title: Wrap Async Setup Components in Suspense for Testing
impact: HIGH
impactDescription: Components with async setup() fail to render in tests without Suspense wrapper, causing cryptic errors
type: gotcha
tags: [vue3, testing, suspense, async-setup, vue-test-utils, vitest]
---
# Wrap Async Setup Components in Suspense for Testing
**Impact: HIGH** - Components using `async setup()` require a `<Suspense>` wrapper to function correctly. Testing them without Suspense causes the component to never render, leading to test failures and confusing errors.
Create a test wrapper component with Suspense or use a `mountSuspense` helper function for testing async components.
## Task Checklist
- [ ] Identify components with async setup (uses `await` in `<script setup>` or `async setup()`)
- [ ] Create a wrapper component with `<Suspense>` for testing
- [ ] Use `flushPromises()` after mounting to wait for async resolution
- [ ] Access the actual component via `findComponent()` for assertions
- [ ] Consider using `@testing-library/vue` with caution (has Suspense issues)
**Incorrect:**
```javascript
import { mount } from '@vue/test-utils'
import AsyncUserProfile from './AsyncUserProfile.vue'
// BAD: Async component without Suspense wrapper
test('displays user data', async () => {
// This won't render - Vue expects Suspense wrapper for async setup
const wrapper = mount(AsyncUserProfile, {
props: { userId: 1 }
})
await flushPromises()
// This fails - component never rendered
expect(wrapper.find('.username').text()).toBe('John')
})
```
**Correct - Manual Wrapper Component:**
```javascript
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, Suspense } from 'vue'
import AsyncUserProfile from './AsyncUserProfile.vue'
test('displays user data', async () => {
// Create wrapper component with Suspense
const TestWrapper = defineComponent({
components: { AsyncUserProfile },
template: `
<Suspense>
<AsyncUserProfile :user-id="1" />
<template #fallback>Loading...</template>
</Suspense>
`
})
const wrapper = mount(TestWrapper)
// Initially shows fallback
expect(wrapper.text()).toContain('Loading...')
// Wait for async setup to complete
await flushPromises()
// Find the actual component for detailed assertions
const profile = wrapper.findComponent(AsyncUserProfile)
expect(profile.find('.username').text()).toBe('John')
})
```
**Correct - Reusable Helper Function:**
```javascript
// test-utils.js
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, Suspense, h } from 'vue'
export async function mountSuspense(component, options = {}) {
const { props, slots, ...mountOptions } = options
const wrapper = mount(
defineComponent({
render() {
return h(
Suspense,
null,
{
default: () => h(component, props, slots),
fallback: () => h('div', 'Loading...')
}
)
}
}),
mountOptions
)
// Wait for async component to resolve
await flushPromises()
return {
wrapper,
// Provide easy access to the actual component
component: wrapper.findComponent(component)
}
}
```
```javascript
// AsyncUserProfile.test.js
import { mountSuspense } from './test-utils'
import AsyncUserProfile from './AsyncUserProfile.vue'
test('displays user data', async () => {
const { component } = await mountSuspense(AsyncUserProfile, {
props: { userId: 1 },
global: {
stubs: {
// Stub any child components if needed
}
}
})
expect(component.find('.username').text()).toBe('John')
})
test('handles errors gracefully', async () => {
const { component } = await mountSuspense(AsyncUserProfile, {
props: { userId: 'invalid' }
})
expect(component.find('.error').exists()).toBe(true)
})
```
## Testing with onErrorCaptured
```javascript
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, Suspense, h, ref, onErrorCaptured } from 'vue'
import AsyncComponent from './AsyncComponent.vue'
test('catches async errors', async () => {
const capturedError = ref(null)
const TestWrapper = defineComponent({
setup() {
onErrorCaptured((error) => {
capturedError.value = error
return true // Prevent error propagation
})
return { capturedError }
},
render() {
return h(Suspense, null, {
default: () => h(AsyncComponent, { shouldFail: true }),
fallback: () => h('div', 'Loading...')
})
}
})
const wrapper = mount(TestWrapper)
await flushPromises()
expect(capturedError.value).toBeTruthy()
expect(capturedError.value.message).toContain('Failed to load')
})
```
## Using with Nuxt's mountSuspended
```javascript
// If using Nuxt, use the built-in mountSuspended helper
import { mountSuspended } from '@nuxt/test-utils/runtime'
import AsyncPage from './AsyncPage.vue'
test('renders async page', async () => {
const wrapper = await mountSuspended(AsyncPage, {
props: { id: 1 }
})
expect(wrapper.find('h1').text()).toBe('Page Title')
})
```
## Important Caveats
### @testing-library/vue Limitation
```javascript
// CAUTION: @testing-library/vue has issues with Suspense
// Use @vue/test-utils for async components instead
// If you must use Testing Library, create manual wrapper:
import { render, waitFor } from '@testing-library/vue'
test('async component with testing library', async () => {
const TestWrapper = {
template: `
<Suspense>
<AsyncComponent />
</Suspense>
`,
components: { AsyncComponent }
}
const { getByText } = render(TestWrapper)
await waitFor(() => {
expect(getByText('Loaded content')).toBeInTheDocument()
})
})
```
### Accessing Component Instance
```javascript
test('access vm on async component', async () => {
const { wrapper, component } = await mountSuspense(AsyncComponent)
// The wrapper.vm is the Suspense wrapper - not useful
// Use component.vm for the actual async component
expect(component.vm.someData).toBe('value')
})
```
## Reference
- [Vue Test Utils - Async Suspense](https://test-utils.vuejs.org/guide/advanced/async-suspense)
- [Vue.js Suspense Documentation](https://vuejs.org/guide/built-ins/suspense.html)
- [Testing Library Vue Suspense Issue](https://github.com/testing-library/vue-testing-library/issues/230)
@@ -0,0 +1,204 @@
---
title: Use Vitest for Vue 3 Testing - Recommended by Vue Team
impact: MEDIUM
impactDescription: Using Jest or other runners with Vite projects requires complex configuration and causes slower test runs
type: best-practice
tags: [vue3, testing, vitest, vite, configuration, setup]
---
# Use Vitest for Vue 3 Testing - Recommended by Vue Team
**Impact: MEDIUM** - Vitest is created and maintained by Vue/Vite team members and shares the same configuration and transform pipeline as Vite. Using Jest or other test runners with Vite-based projects requires additional configuration and can result in slower test execution and compatibility issues.
Use Vitest for new Vue 3 projects. Only consider Jest if migrating an existing test suite.
## Task Checklist
- [ ] Install Vitest and related packages for Vue testing
- [ ] Configure vitest in vite.config.js or vitest.config.js
- [ ] Set up proper test environment (happy-dom or jsdom)
- [ ] Add test scripts to package.json
- [ ] Configure globals if desired for cleaner test syntax
- [ ] Use @vue/test-utils for component mounting
## Quick Setup
```bash
# Install required packages
npm install -D vitest @vue/test-utils happy-dom
# or with jsdom
npm install -D vitest @vue/test-utils jsdom
```
**vite.config.js:**
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
// Enable global test APIs (describe, it, expect)
globals: true,
// Use happy-dom for faster tests (or 'jsdom' for better compatibility)
environment: 'happy-dom',
// Optional: Setup files for global configuration
setupFiles: ['./src/test/setup.js']
}
})
```
**package.json:**
```json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
```
**tsconfig.json (if using TypeScript):**
```json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
```
## Test File Example
```javascript
// src/components/Counter.test.js
import { describe, it, expect, beforeEach } from 'vitest' // optional with globals: true
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
let wrapper
beforeEach(() => {
wrapper = mount(Counter)
})
it('renders initial count', () => {
expect(wrapper.find('[data-testid="count"]').text()).toBe('0')
})
it('increments when button clicked', async () => {
await wrapper.find('[data-testid="increment"]').trigger('click')
expect(wrapper.find('[data-testid="count"]').text()).toBe('1')
})
})
```
## Vitest vs Jest Comparison
| Feature | Vitest | Jest |
|---------|--------|------|
| Vite Integration | Native | Requires config |
| Speed | Very fast (ESM native) | Slower with Vite |
| Watch Mode | Excellent | Good |
| Vue SFC Support | Works with Vite | Needs vue-jest |
| Config Sharing | Same as vite.config | Separate |
| API | Jest-compatible | Standard |
## Using with Testing Library
```bash
npm install -D @testing-library/vue @testing-library/jest-dom
```
```javascript
// src/test/setup.js
import { expect } from 'vitest'
import * as matchers from '@testing-library/jest-dom/matchers'
expect.extend(matchers)
```
```javascript
// Component.test.js
import { render, screen, fireEvent } from '@testing-library/vue'
import UserCard from './UserCard.vue'
test('displays user name', () => {
render(UserCard, {
props: { name: 'John Doe' }
})
expect(screen.getByText('John Doe')).toBeInTheDocument()
})
```
## Advanced Configuration
```javascript
// vitest.config.js (separate file if preferred)
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'happy-dom',
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
exclude: ['node_modules', 'dist', 'e2e'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules', 'test']
},
// Helpful for debugging
reporters: ['verbose'],
// Run tests in sequence in CI
poolOptions: {
threads: {
singleThread: process.env.CI === 'true'
}
}
}
})
```
## Common Patterns
### Mocking Modules
```javascript
import { vi } from 'vitest'
vi.mock('@/api/users', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'John' })
}))
```
### Testing with Fake Timers
```javascript
import { vi, beforeEach, afterEach } from 'vitest'
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
})
test('debounced search', async () => {
const wrapper = mount(SearchBox)
await wrapper.find('input').setValue('vue')
vi.advanceTimersByTime(300)
await flushPromises()
expect(wrapper.emitted('search')).toBeTruthy()
})
```
## Reference
- [Vitest Documentation](https://vitest.dev/)
- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)
- [Vue Test Utils](https://test-utils.vuejs.org/)
@@ -0,0 +1,39 @@
---
name: web-design-guidelines
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
metadata:
author: vercel
version: "1.0.0"
argument-hint: <file-or-pattern>
---
# Web Interface Guidelines
Review files for compliance with Web Interface Guidelines.
## How It Works
1. Fetch the latest guidelines from the source URL below
2. Read the specified files (or prompt user for files/pattern)
3. Check against all rules in the fetched guidelines
4. Output findings in the terse `file:line` format
## Guidelines Source
Fetch fresh guidelines before each review:
```
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
```
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
## Usage
When a user provides a file or pattern argument:
1. Fetch guidelines from the source URL above
2. Read the specified files
3. Apply all rules from the fetched guidelines
4. Output findings using the format specified in the guidelines
If no files specified, ask the user which files to review.
@@ -0,0 +1,5 @@
# Sync Info
- **Source:** `vendor/web-design-guidelines/skills/web-design-guidelines`
- **Git SHA:** `5847a7c7e79bab3e400cf47800b83449d7aea2d4`
- **Synced:** 2026-03-16
+24
View File
@@ -11,11 +11,23 @@
"sourceType": "github",
"computedHash": "e28297714fd27dc7c3ebe2f3ab4c9f23d9e1e92abbb00248ca7878d833f190ea"
},
"pinia": {
"source": "antfu/skills",
"sourceType": "github",
"skillPath": "skills/pinia/SKILL.md",
"computedHash": "897b6b0982956f41f5cc85128ca638b625e1b3c8bc619e303cd1e7661c95b7de"
},
"pnpm": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "318f9fca2441a3e06fedc336e195dd18be5dbe901064e4fa71b76b40b096ab3a"
},
"tsdown": {
"source": "antfu/skills",
"sourceType": "github",
"skillPath": "skills/tsdown/SKILL.md",
"computedHash": "2183b9019058744215ab8b1261e34833653b5864da159bc022e3b9ad2f706aa5"
},
"unocss": {
"source": "antfu/skills",
"sourceType": "github",
@@ -31,11 +43,23 @@
"sourceType": "github",
"computedHash": "9200aa5cdf61032bd7d870a08a269a9a8ab672da6e594e7d4318ee3f23bde31d"
},
"vue-testing-best-practices": {
"source": "antfu/skills",
"sourceType": "github",
"skillPath": "skills/vue-testing-best-practices/SKILL.md",
"computedHash": "18c7d8f42f350f927e37de055e34c97b8cfb9f79c12cf942f7f3d2a0821057b5"
},
"vueuse-functions": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "4073414438ae18f65782f968947e614046474d6e31d97c508b627cd80abb1975"
},
"web-design-guidelines": {
"source": "antfu/skills",
"sourceType": "github",
"skillPath": "skills/web-design-guidelines/SKILL.md",
"computedHash": "65a2e7d85753383ae0f88df15475d58b9e39723e9c4bb6891421d6144a85f79c"
},
"xsai": {
"source": "moeru-ai/xsai",
"sourceType": "github",