mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 00:48:06 +00:00
chore(.agents/skills): added more useful skills, remove old one
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Generation Info
|
||||
|
||||
- **Source:** `sources/pinia`
|
||||
- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e`
|
||||
- **Generated:** 2026-01-28
|
||||
@@ -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
|
||||
-->
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
# Sync Info
|
||||
|
||||
- **Source:** `vendor/tsdown/skills/tsdown`
|
||||
- **Git SHA:** `f635a43b3c8b18569b47f3789c801f44a45c668a`
|
||||
- **Synced:** 2026-06-22
|
||||
@@ -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)
|
||||
+175
@@ -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)
|
||||
+208
@@ -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)
|
||||
+144
@@ -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)
|
||||
+238
@@ -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/)
|
||||
+242
@@ -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)
|
||||
+229
@@ -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)
|
||||
+204
@@ -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
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user