Add Claude Code skills for web frontend frameworks

- Add Svelte 3/4 skill covering components, reactivity, stores, lifecycle
- Add Rollup skill covering configuration, plugins, code splitting
- Add nostr-tools skill covering event creation, signing, relay communication
- Add applesauce-core skill covering event stores, reactive queries
- Add applesauce-signers skill covering NIP-07/NIP-46 signing abstractions
- Update .gitignore to include .claude/** directory

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-06 06:56:57 +00:00
parent d3d2d6e643
commit 8ea91e39d8
7 changed files with 4066 additions and 1 deletions

View File

@@ -0,0 +1,634 @@
---
name: applesauce-core
description: This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications.
---
# applesauce-core Skill
This skill provides comprehensive knowledge and patterns for working with applesauce-core, a library that provides reactive utilities and patterns for building Nostr clients.
## When to Use This Skill
Use this skill when:
- Building reactive Nostr applications
- Managing event stores and caches
- Working with observable patterns for Nostr
- Implementing real-time updates
- Building timeline and feed views
- Managing replaceable events
- Working with profiles and metadata
- Creating efficient Nostr queries
## Core Concepts
### applesauce-core Overview
applesauce-core provides:
- **Event stores** - Reactive event caching and management
- **Queries** - Declarative event querying patterns
- **Observables** - RxJS-based reactive patterns
- **Profile helpers** - Profile metadata management
- **Timeline utilities** - Feed and timeline building
- **NIP helpers** - NIP-specific utilities
### Installation
```bash
npm install applesauce-core
```
### Basic Architecture
applesauce-core is built on reactive principles:
- Events are stored in reactive stores
- Queries return observables that update when new events arrive
- Components subscribe to observables for real-time updates
## Event Store
### Creating an Event Store
```javascript
import { EventStore } from 'applesauce-core';
// Create event store
const eventStore = new EventStore();
// Add events
eventStore.add(event1);
eventStore.add(event2);
// Add multiple events
eventStore.addMany([event1, event2, event3]);
// Check if event exists
const exists = eventStore.has(eventId);
// Get event by ID
const event = eventStore.get(eventId);
// Remove event
eventStore.remove(eventId);
// Clear all events
eventStore.clear();
```
### Event Store Queries
```javascript
// Get all events
const allEvents = eventStore.getAll();
// Get events by filter
const filtered = eventStore.filter({
kinds: [1],
authors: [pubkey]
});
// Get events by author
const authorEvents = eventStore.getByAuthor(pubkey);
// Get events by kind
const textNotes = eventStore.getByKind(1);
```
### Replaceable Events
applesauce-core handles replaceable events automatically:
```javascript
// For kind 0 (profile), only latest is kept
eventStore.add(profileEvent1); // stored
eventStore.add(profileEvent2); // replaces if newer
// For parameterized replaceable (30000-39999)
eventStore.add(articleEvent); // keyed by author + kind + d-tag
// Get replaceable event
const profile = eventStore.getReplaceable(0, pubkey);
const article = eventStore.getReplaceable(30023, pubkey, 'article-slug');
```
## Queries
### Query Patterns
```javascript
import { createQuery } from 'applesauce-core';
// Create a query
const query = createQuery(eventStore, {
kinds: [1],
limit: 50
});
// Subscribe to query results
query.subscribe(events => {
console.log('Current events:', events);
});
// Query updates automatically when new events added
eventStore.add(newEvent); // Subscribers notified
```
### Timeline Query
```javascript
import { TimelineQuery } from 'applesauce-core';
// Create timeline for user's notes
const timeline = new TimelineQuery(eventStore, {
kinds: [1],
authors: [userPubkey]
});
// Get observable of timeline
const timeline$ = timeline.events$;
// Subscribe
timeline$.subscribe(events => {
// Events sorted by created_at, newest first
renderTimeline(events);
});
```
### Profile Query
```javascript
import { ProfileQuery } from 'applesauce-core';
// Query profile metadata
const profileQuery = new ProfileQuery(eventStore, pubkey);
// Get observable
const profile$ = profileQuery.profile$;
profile$.subscribe(profile => {
if (profile) {
console.log('Name:', profile.name);
console.log('Picture:', profile.picture);
}
});
```
## Observables
### Working with RxJS
applesauce-core uses RxJS observables:
```javascript
import { map, filter, distinctUntilChanged } from 'rxjs/operators';
// Transform query results
const names$ = profileQuery.profile$.pipe(
filter(profile => profile !== null),
map(profile => profile.name),
distinctUntilChanged()
);
// Combine multiple observables
import { combineLatest } from 'rxjs';
const combined$ = combineLatest([
timeline$,
profile$
]).pipe(
map(([events, profile]) => ({
events,
authorName: profile?.name
}))
);
```
### Creating Custom Observables
```javascript
import { Observable } from 'rxjs';
function createEventObservable(store, filter) {
return new Observable(subscriber => {
// Initial emit
subscriber.next(store.filter(filter));
// Subscribe to store changes
const unsubscribe = store.onChange(() => {
subscriber.next(store.filter(filter));
});
// Cleanup
return () => unsubscribe();
});
}
```
## Profile Helpers
### Profile Metadata
```javascript
import { parseProfile, ProfileContent } from 'applesauce-core';
// Parse kind 0 content
const profileEvent = await getProfileEvent(pubkey);
const profile = parseProfile(profileEvent);
// Profile fields
console.log(profile.name); // Display name
console.log(profile.about); // Bio
console.log(profile.picture); // Avatar URL
console.log(profile.banner); // Banner image URL
console.log(profile.nip05); // NIP-05 identifier
console.log(profile.lud16); // Lightning address
console.log(profile.website); // Website URL
```
### Profile Store
```javascript
import { ProfileStore } from 'applesauce-core';
const profileStore = new ProfileStore(eventStore);
// Get profile observable
const profile$ = profileStore.getProfile(pubkey);
// Get multiple profiles
const profiles$ = profileStore.getProfiles([pubkey1, pubkey2]);
// Request profile load (triggers fetch if not cached)
profileStore.requestProfile(pubkey);
```
## Timeline Utilities
### Building Feeds
```javascript
import { Timeline } from 'applesauce-core';
// Create timeline
const timeline = new Timeline(eventStore);
// Add filter
timeline.setFilter({
kinds: [1, 6],
authors: followedPubkeys
});
// Get events observable
const events$ = timeline.events$;
// Load more (pagination)
timeline.loadMore(50);
// Refresh (get latest)
timeline.refresh();
```
### Thread Building
```javascript
import { ThreadBuilder } from 'applesauce-core';
// Build thread from root event
const thread = new ThreadBuilder(eventStore, rootEventId);
// Get thread observable
const thread$ = thread.thread$;
thread$.subscribe(threadData => {
console.log('Root:', threadData.root);
console.log('Replies:', threadData.replies);
console.log('Reply count:', threadData.replyCount);
});
```
### Reactions and Zaps
```javascript
import { ReactionStore, ZapStore } from 'applesauce-core';
// Reactions
const reactionStore = new ReactionStore(eventStore);
const reactions$ = reactionStore.getReactions(eventId);
reactions$.subscribe(reactions => {
console.log('Likes:', reactions.likes);
console.log('Custom:', reactions.custom);
});
// Zaps
const zapStore = new ZapStore(eventStore);
const zaps$ = zapStore.getZaps(eventId);
zaps$.subscribe(zaps => {
console.log('Total sats:', zaps.totalAmount);
console.log('Zap count:', zaps.count);
});
```
## NIP Helpers
### NIP-05 Verification
```javascript
import { verifyNip05 } from 'applesauce-core';
// Verify NIP-05
const result = await verifyNip05('alice@example.com', expectedPubkey);
if (result.valid) {
console.log('NIP-05 verified');
} else {
console.log('Verification failed:', result.error);
}
```
### NIP-10 Reply Parsing
```javascript
import { parseReplyTags } from 'applesauce-core';
// Parse reply structure
const parsed = parseReplyTags(event);
console.log('Root event:', parsed.root);
console.log('Reply to:', parsed.reply);
console.log('Mentions:', parsed.mentions);
```
### NIP-65 Relay Lists
```javascript
import { parseRelayList } from 'applesauce-core';
// Parse relay list event (kind 10002)
const relays = parseRelayList(relayListEvent);
console.log('Read relays:', relays.read);
console.log('Write relays:', relays.write);
```
## Integration with nostr-tools
### Using with SimplePool
```javascript
import { SimplePool } from 'nostr-tools';
import { EventStore } from 'applesauce-core';
const pool = new SimplePool();
const eventStore = new EventStore();
// Load events into store
pool.subscribeMany(relays, [filter], {
onevent(event) {
eventStore.add(event);
}
});
// Query store reactively
const timeline$ = createTimelineQuery(eventStore, filter);
```
### Publishing Events
```javascript
import { finalizeEvent } from 'nostr-tools';
// Create event
const event = finalizeEvent({
kind: 1,
content: 'Hello!',
created_at: Math.floor(Date.now() / 1000),
tags: []
}, secretKey);
// Add to local store immediately (optimistic update)
eventStore.add(event);
// Publish to relays
await pool.publish(relays, event);
```
## Svelte Integration
### Using in Svelte Components
```svelte
<script>
import { onMount, onDestroy } from 'svelte';
import { EventStore, TimelineQuery } from 'applesauce-core';
export let pubkey;
const eventStore = new EventStore();
let events = [];
let subscription;
onMount(() => {
const timeline = new TimelineQuery(eventStore, {
kinds: [1],
authors: [pubkey]
});
subscription = timeline.events$.subscribe(e => {
events = e;
});
});
onDestroy(() => {
subscription?.unsubscribe();
});
</script>
{#each events as event}
<div class="event">
{event.content}
</div>
{/each}
```
### Svelte Store Adapter
```javascript
import { readable } from 'svelte/store';
// Convert RxJS observable to Svelte store
function fromObservable(observable, initialValue) {
return readable(initialValue, set => {
const subscription = observable.subscribe(set);
return () => subscription.unsubscribe();
});
}
// Usage
const events$ = timeline.events$;
const eventsStore = fromObservable(events$, []);
```
```svelte
<script>
import { eventsStore } from './stores.js';
</script>
{#each $eventsStore as event}
<div>{event.content}</div>
{/each}
```
## Best Practices
### Store Management
1. **Single store instance** - Use one EventStore per app
2. **Clear stale data** - Implement cache limits
3. **Handle replaceable events** - Let store manage deduplication
4. **Unsubscribe** - Clean up subscriptions on component destroy
### Query Optimization
1. **Use specific filters** - Narrow queries perform better
2. **Limit results** - Use limit for initial loads
3. **Cache queries** - Reuse query instances
4. **Debounce updates** - Throttle rapid changes
### Memory Management
1. **Limit store size** - Implement LRU or time-based eviction
2. **Clean up observables** - Unsubscribe when done
3. **Use weak references** - For profile caches
4. **Paginate large feeds** - Don't load everything at once
### Reactive Patterns
1. **Prefer observables** - Over imperative queries
2. **Use operators** - Transform data with RxJS
3. **Combine streams** - For complex views
4. **Handle loading states** - Show placeholders
## Common Patterns
### Event Deduplication
```javascript
// EventStore handles deduplication automatically
eventStore.add(event1);
eventStore.add(event1); // No duplicate
// For manual deduplication
const seen = new Set();
events.filter(e => {
if (seen.has(e.id)) return false;
seen.add(e.id);
return true;
});
```
### Optimistic Updates
```javascript
async function publishNote(content) {
// Create event
const event = await createEvent(content);
// Add to store immediately (optimistic)
eventStore.add(event);
try {
// Publish to relays
await pool.publish(relays, event);
} catch (error) {
// Remove on failure
eventStore.remove(event.id);
throw error;
}
}
```
### Loading States
```javascript
import { BehaviorSubject, combineLatest } from 'rxjs';
const loading$ = new BehaviorSubject(true);
const events$ = timeline.events$;
const state$ = combineLatest([loading$, events$]).pipe(
map(([loading, events]) => ({
loading,
events,
empty: !loading && events.length === 0
}))
);
// Start loading
loading$.next(true);
await loadEvents();
loading$.next(false);
```
### Infinite Scroll
```javascript
function createInfiniteScroll(timeline, pageSize = 50) {
let loading = false;
async function loadMore() {
if (loading) return;
loading = true;
await timeline.loadMore(pageSize);
loading = false;
}
function onScroll(event) {
const { scrollTop, scrollHeight, clientHeight } = event.target;
if (scrollHeight - scrollTop <= clientHeight * 1.5) {
loadMore();
}
}
return { loadMore, onScroll };
}
```
## Troubleshooting
### Common Issues
**Events not updating:**
- Check subscription is active
- Verify events are being added to store
- Ensure filter matches events
**Memory growing:**
- Implement store size limits
- Clean up subscriptions
- Use weak references where appropriate
**Slow queries:**
- Add indexes for common queries
- Use more specific filters
- Implement pagination
**Stale data:**
- Implement refresh mechanisms
- Set up real-time subscriptions
- Handle replaceable event updates
## References
- **applesauce GitHub**: https://github.com/hzrd149/applesauce
- **RxJS Documentation**: https://rxjs.dev
- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools
- **Nostr Protocol**: https://github.com/nostr-protocol/nostr
## Related Skills
- **nostr-tools** - Lower-level Nostr operations
- **applesauce-signers** - Event signing abstractions
- **svelte** - Building reactive UIs
- **nostr** - Nostr protocol fundamentals

View File

@@ -0,0 +1,757 @@
---
name: applesauce-signers
description: This skill should be used when working with applesauce-signers library for Nostr event signing, including NIP-07 browser extensions, NIP-46 remote signing, and custom signer implementations. Provides comprehensive knowledge of signing patterns and signer abstractions.
---
# applesauce-signers Skill
This skill provides comprehensive knowledge and patterns for working with applesauce-signers, a library that provides signing abstractions for Nostr applications.
## When to Use This Skill
Use this skill when:
- Implementing event signing in Nostr applications
- Integrating with NIP-07 browser extensions
- Working with NIP-46 remote signers
- Building custom signer implementations
- Managing signing sessions
- Handling signing requests and permissions
- Implementing multi-signer support
## Core Concepts
### applesauce-signers Overview
applesauce-signers provides:
- **Signer abstraction** - Unified interface for different signers
- **NIP-07 integration** - Browser extension support
- **NIP-46 support** - Remote signing (Nostr Connect)
- **Simple signers** - Direct key signing
- **Permission handling** - Manage signing requests
- **Observable patterns** - Reactive signing states
### Installation
```bash
npm install applesauce-signers
```
### Signer Interface
All signers implement a common interface:
```typescript
interface Signer {
// Get public key
getPublicKey(): Promise<string>;
// Sign event
signEvent(event: UnsignedEvent): Promise<SignedEvent>;
// Encrypt (NIP-04)
nip04Encrypt?(pubkey: string, plaintext: string): Promise<string>;
nip04Decrypt?(pubkey: string, ciphertext: string): Promise<string>;
// Encrypt (NIP-44)
nip44Encrypt?(pubkey: string, plaintext: string): Promise<string>;
nip44Decrypt?(pubkey: string, ciphertext: string): Promise<string>;
}
```
## Simple Signer
### Using Secret Key
```javascript
import { SimpleSigner } from 'applesauce-signers';
import { generateSecretKey } from 'nostr-tools';
// Create signer with existing key
const signer = new SimpleSigner(secretKey);
// Or generate new key
const newSecretKey = generateSecretKey();
const newSigner = new SimpleSigner(newSecretKey);
// Get public key
const pubkey = await signer.getPublicKey();
// Sign event
const unsignedEvent = {
kind: 1,
content: 'Hello Nostr!',
created_at: Math.floor(Date.now() / 1000),
tags: []
};
const signedEvent = await signer.signEvent(unsignedEvent);
```
### NIP-04 Encryption
```javascript
// Encrypt message
const ciphertext = await signer.nip04Encrypt(
recipientPubkey,
'Secret message'
);
// Decrypt message
const plaintext = await signer.nip04Decrypt(
senderPubkey,
ciphertext
);
```
### NIP-44 Encryption
```javascript
// Encrypt with NIP-44 (preferred)
const ciphertext = await signer.nip44Encrypt(
recipientPubkey,
'Secret message'
);
// Decrypt
const plaintext = await signer.nip44Decrypt(
senderPubkey,
ciphertext
);
```
## NIP-07 Signer
### Browser Extension Integration
```javascript
import { Nip07Signer } from 'applesauce-signers';
// Check if extension is available
if (window.nostr) {
const signer = new Nip07Signer();
// Get public key (may prompt user)
const pubkey = await signer.getPublicKey();
// Sign event (prompts user)
const signedEvent = await signer.signEvent(unsignedEvent);
}
```
### Handling Extension Availability
```javascript
function getAvailableSigner() {
if (typeof window !== 'undefined' && window.nostr) {
return new Nip07Signer();
}
return null;
}
// Wait for extension to load
async function waitForExtension(timeout = 3000) {
const start = Date.now();
while (Date.now() - start < timeout) {
if (window.nostr) {
return new Nip07Signer();
}
await new Promise(r => setTimeout(r, 100));
}
return null;
}
```
### Extension Permissions
```javascript
// Some extensions support granular permissions
const signer = new Nip07Signer();
// Request specific permissions
try {
// This varies by extension
await window.nostr.enable();
} catch (error) {
console.log('User denied permission');
}
```
## NIP-46 Remote Signer
### Nostr Connect
```javascript
import { Nip46Signer } from 'applesauce-signers';
// Create remote signer
const signer = new Nip46Signer({
// Remote signer's pubkey
remotePubkey: signerPubkey,
// Relays for communication
relays: ['wss://relay.example.com'],
// Local secret key for encryption
localSecretKey: localSecretKey,
// Optional: custom client name
clientName: 'My Nostr App'
});
// Connect to remote signer
await signer.connect();
// Get public key
const pubkey = await signer.getPublicKey();
// Sign event
const signedEvent = await signer.signEvent(unsignedEvent);
// Disconnect when done
signer.disconnect();
```
### Connection URL
```javascript
// Parse nostrconnect:// URL
function parseNostrConnectUrl(url) {
const parsed = new URL(url);
return {
pubkey: parsed.pathname.replace('//', ''),
relay: parsed.searchParams.get('relay'),
secret: parsed.searchParams.get('secret')
};
}
// Create signer from URL
const { pubkey, relay, secret } = parseNostrConnectUrl(connectUrl);
const signer = new Nip46Signer({
remotePubkey: pubkey,
relays: [relay],
localSecretKey: generateSecretKey(),
secret: secret
});
```
### Bunker URL
```javascript
// Parse bunker:// URL (NIP-46)
function parseBunkerUrl(url) {
const parsed = new URL(url);
return {
pubkey: parsed.pathname.replace('//', ''),
relays: parsed.searchParams.getAll('relay'),
secret: parsed.searchParams.get('secret')
};
}
const { pubkey, relays, secret } = parseBunkerUrl(bunkerUrl);
```
## Signer Management
### Signer Store
```javascript
import { SignerStore } from 'applesauce-signers';
const signerStore = new SignerStore();
// Set active signer
signerStore.setSigner(signer);
// Get active signer
const activeSigner = signerStore.getSigner();
// Clear signer (logout)
signerStore.clearSigner();
// Observable for signer changes
signerStore.signer$.subscribe(signer => {
if (signer) {
console.log('Logged in');
} else {
console.log('Logged out');
}
});
```
### Multi-Account Support
```javascript
class AccountManager {
constructor() {
this.accounts = new Map();
this.activeAccount = null;
}
addAccount(pubkey, signer) {
this.accounts.set(pubkey, signer);
}
removeAccount(pubkey) {
this.accounts.delete(pubkey);
if (this.activeAccount === pubkey) {
this.activeAccount = null;
}
}
switchAccount(pubkey) {
if (this.accounts.has(pubkey)) {
this.activeAccount = pubkey;
return this.accounts.get(pubkey);
}
return null;
}
getActiveSigner() {
return this.activeAccount
? this.accounts.get(this.activeAccount)
: null;
}
}
```
## Custom Signers
### Implementing a Custom Signer
```javascript
class CustomSigner {
constructor(options) {
this.options = options;
}
async getPublicKey() {
// Return public key
return this.options.pubkey;
}
async signEvent(event) {
// Implement signing logic
// Could call external API, hardware wallet, etc.
const signedEvent = await this.externalSign(event);
return signedEvent;
}
async nip04Encrypt(pubkey, plaintext) {
// Implement NIP-04 encryption
throw new Error('NIP-04 not supported');
}
async nip04Decrypt(pubkey, ciphertext) {
throw new Error('NIP-04 not supported');
}
async nip44Encrypt(pubkey, plaintext) {
// Implement NIP-44 encryption
throw new Error('NIP-44 not supported');
}
async nip44Decrypt(pubkey, ciphertext) {
throw new Error('NIP-44 not supported');
}
}
```
### Hardware Wallet Signer
```javascript
class HardwareWalletSigner {
constructor(devicePath) {
this.devicePath = devicePath;
}
async connect() {
// Connect to hardware device
this.device = await connectToDevice(this.devicePath);
}
async getPublicKey() {
// Get public key from device
return await this.device.getNostrPubkey();
}
async signEvent(event) {
// Sign on device (user confirms on device)
const signature = await this.device.signNostrEvent(event);
return {
...event,
pubkey: await this.getPublicKey(),
id: getEventHash(event),
sig: signature
};
}
}
```
### Read-Only Signer
```javascript
class ReadOnlySigner {
constructor(pubkey) {
this.pubkey = pubkey;
}
async getPublicKey() {
return this.pubkey;
}
async signEvent(event) {
throw new Error('Read-only mode: cannot sign events');
}
async nip04Encrypt(pubkey, plaintext) {
throw new Error('Read-only mode: cannot encrypt');
}
async nip04Decrypt(pubkey, ciphertext) {
throw new Error('Read-only mode: cannot decrypt');
}
}
```
## Signing Utilities
### Event Creation Helper
```javascript
async function createAndSignEvent(signer, template) {
const pubkey = await signer.getPublicKey();
const event = {
...template,
pubkey,
created_at: template.created_at || Math.floor(Date.now() / 1000)
};
return await signer.signEvent(event);
}
// Usage
const signedNote = await createAndSignEvent(signer, {
kind: 1,
content: 'Hello!',
tags: []
});
```
### Batch Signing
```javascript
async function signEvents(signer, events) {
const signed = [];
for (const event of events) {
const signedEvent = await signer.signEvent(event);
signed.push(signedEvent);
}
return signed;
}
// With parallelization (if signer supports)
async function signEventsParallel(signer, events) {
return Promise.all(
events.map(event => signer.signEvent(event))
);
}
```
## Svelte Integration
### Signer Context
```svelte
<!-- SignerProvider.svelte -->
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
const signer = writable(null);
setContext('signer', {
signer,
setSigner: (s) => signer.set(s),
clearSigner: () => signer.set(null)
});
</script>
<slot />
```
```svelte
<!-- Component using signer -->
<script>
import { getContext } from 'svelte';
const { signer } = getContext('signer');
async function publishNote(content) {
if (!$signer) {
alert('Please login first');
return;
}
const event = await $signer.signEvent({
kind: 1,
content,
created_at: Math.floor(Date.now() / 1000),
tags: []
});
// Publish event...
}
</script>
```
### Login Component
```svelte
<script>
import { getContext } from 'svelte';
import { Nip07Signer, SimpleSigner } from 'applesauce-signers';
const { setSigner, clearSigner, signer } = getContext('signer');
let nsec = '';
async function loginWithExtension() {
if (window.nostr) {
setSigner(new Nip07Signer());
} else {
alert('No extension found');
}
}
function loginWithNsec() {
try {
const decoded = nip19.decode(nsec);
if (decoded.type === 'nsec') {
setSigner(new SimpleSigner(decoded.data));
nsec = '';
}
} catch (e) {
alert('Invalid nsec');
}
}
function logout() {
clearSigner();
}
</script>
{#if $signer}
<button on:click={logout}>Logout</button>
{:else}
<button on:click={loginWithExtension}>
Login with Extension
</button>
<div>
<input
type="password"
bind:value={nsec}
placeholder="nsec..."
/>
<button on:click={loginWithNsec}>
Login with Key
</button>
</div>
{/if}
```
## Best Practices
### Security
1. **Never store secret keys in plain text** - Use secure storage
2. **Prefer NIP-07** - Let extensions manage keys
3. **Clear keys on logout** - Don't leave in memory
4. **Validate before signing** - Check event content
### User Experience
1. **Show signing status** - Loading states
2. **Handle rejections gracefully** - User may cancel
3. **Provide fallbacks** - Multiple login options
4. **Remember preferences** - Store signer type
### Error Handling
```javascript
async function safeSign(signer, event) {
try {
return await signer.signEvent(event);
} catch (error) {
if (error.message.includes('rejected')) {
console.log('User rejected signing');
return null;
}
if (error.message.includes('timeout')) {
console.log('Signing timed out');
return null;
}
throw error;
}
}
```
### Permission Checking
```javascript
function hasEncryptionSupport(signer) {
return typeof signer.nip04Encrypt === 'function' ||
typeof signer.nip44Encrypt === 'function';
}
function getEncryptionMethod(signer) {
// Prefer NIP-44
if (typeof signer.nip44Encrypt === 'function') {
return 'nip44';
}
if (typeof signer.nip04Encrypt === 'function') {
return 'nip04';
}
return null;
}
```
## Common Patterns
### Signer Detection
```javascript
async function detectSigners() {
const available = [];
// Check NIP-07
if (typeof window !== 'undefined' && window.nostr) {
available.push({
type: 'nip07',
name: 'Browser Extension',
create: () => new Nip07Signer()
});
}
// Check stored credentials
const storedKey = localStorage.getItem('nsec');
if (storedKey) {
available.push({
type: 'stored',
name: 'Saved Key',
create: () => new SimpleSigner(storedKey)
});
}
return available;
}
```
### Auto-Reconnect for NIP-46
```javascript
class ReconnectingNip46Signer {
constructor(options) {
this.options = options;
this.signer = null;
}
async connect() {
this.signer = new Nip46Signer(this.options);
await this.signer.connect();
}
async signEvent(event) {
try {
return await this.signer.signEvent(event);
} catch (error) {
if (error.message.includes('disconnected')) {
await this.connect();
return await this.signer.signEvent(event);
}
throw error;
}
}
}
```
### Signer Type Persistence
```javascript
const SIGNER_KEY = 'nostr_signer_type';
function saveSigner(type, data) {
localStorage.setItem(SIGNER_KEY, JSON.stringify({ type, data }));
}
async function restoreSigner() {
const saved = localStorage.getItem(SIGNER_KEY);
if (!saved) return null;
const { type, data } = JSON.parse(saved);
switch (type) {
case 'nip07':
if (window.nostr) {
return new Nip07Signer();
}
break;
case 'simple':
// Don't store secret keys!
break;
case 'nip46':
const signer = new Nip46Signer(data);
await signer.connect();
return signer;
}
return null;
}
```
## Troubleshooting
### Common Issues
**Extension not detected:**
- Wait for page load
- Check window.nostr exists
- Verify extension is enabled
**Signing rejected:**
- User cancelled in extension
- Handle gracefully with error message
**NIP-46 connection fails:**
- Check relay is accessible
- Verify remote signer is online
- Check secret matches
**Encryption not supported:**
- Check signer has encrypt methods
- Fall back to alternative method
- Show user appropriate error
## References
- **applesauce GitHub**: https://github.com/hzrd149/applesauce
- **NIP-07 Specification**: https://github.com/nostr-protocol/nips/blob/master/07.md
- **NIP-46 Specification**: https://github.com/nostr-protocol/nips/blob/master/46.md
- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools
## Related Skills
- **nostr-tools** - Event creation and signing utilities
- **applesauce-core** - Event stores and queries
- **nostr** - Nostr protocol fundamentals
- **svelte** - Building Nostr UIs

View File

@@ -0,0 +1,767 @@
---
name: nostr-tools
description: This skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns.
---
# nostr-tools Skill
This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development.
## When to Use This Skill
Use this skill when:
- Building Nostr clients or applications
- Creating and signing Nostr events
- Connecting to Nostr relays
- Implementing NIP features
- Working with Nostr keys and cryptography
- Filtering and querying events
- Building relay pools or connections
- Implementing NIP-44/NIP-04 encryption
## Core Concepts
### nostr-tools Overview
nostr-tools provides:
- **Event handling** - Create, sign, verify events
- **Key management** - Generate, convert, encode keys
- **Relay communication** - Connect, subscribe, publish
- **NIP implementations** - NIP-04, NIP-05, NIP-19, NIP-44, etc.
- **Cryptographic operations** - Schnorr signatures, encryption
- **Filter building** - Query events by various criteria
### Installation
```bash
npm install nostr-tools
```
### Basic Imports
```javascript
// Core functionality
import {
SimplePool,
generateSecretKey,
getPublicKey,
finalizeEvent,
verifyEvent
} from 'nostr-tools';
// NIP-specific imports
import { nip04, nip05, nip19, nip44 } from 'nostr-tools';
// Relay operations
import { Relay } from 'nostr-tools/relay';
```
## Key Management
### Generating Keys
```javascript
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
// Generate new secret key (Uint8Array)
const secretKey = generateSecretKey();
// Derive public key
const publicKey = getPublicKey(secretKey);
console.log('Secret key:', bytesToHex(secretKey));
console.log('Public key:', publicKey); // hex string
```
### Key Encoding (NIP-19)
```javascript
import { nip19 } from 'nostr-tools';
// Encode to bech32
const nsec = nip19.nsecEncode(secretKey);
const npub = nip19.npubEncode(publicKey);
const note = nip19.noteEncode(eventId);
console.log(nsec); // nsec1...
console.log(npub); // npub1...
console.log(note); // note1...
// Decode from bech32
const { type, data } = nip19.decode(npub);
// type: 'npub', data: publicKey (hex)
// Encode profile reference (nprofile)
const nprofile = nip19.nprofileEncode({
pubkey: publicKey,
relays: ['wss://relay.example.com']
});
// Encode event reference (nevent)
const nevent = nip19.neventEncode({
id: eventId,
relays: ['wss://relay.example.com'],
author: publicKey,
kind: 1
});
// Encode address (naddr) for replaceable events
const naddr = nip19.naddrEncode({
identifier: 'my-article',
pubkey: publicKey,
kind: 30023,
relays: ['wss://relay.example.com']
});
```
## Event Operations
### Event Structure
```javascript
// Unsigned event template
const eventTemplate = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'Hello Nostr!'
};
// Signed event (after finalizeEvent)
const signedEvent = {
id: '...', // 32-byte sha256 hash as hex
pubkey: '...', // 32-byte public key as hex
created_at: 1234567890,
kind: 1,
tags: [],
content: 'Hello Nostr!',
sig: '...' // 64-byte Schnorr signature as hex
};
```
### Creating and Signing Events
```javascript
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure';
// Create event template
const eventTemplate = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', publicKey], // Mention
['e', eventId, '', 'reply'], // Reply
['t', 'nostr'] // Hashtag
],
content: 'Hello Nostr!'
};
// Sign event
const signedEvent = finalizeEvent(eventTemplate, secretKey);
// Verify event
const isValid = verifyEvent(signedEvent);
console.log('Event valid:', isValid);
```
### Event Kinds
```javascript
// Common event kinds
const KINDS = {
Metadata: 0, // Profile metadata (NIP-01)
Text: 1, // Short text note (NIP-01)
RecommendRelay: 2, // Relay recommendation
Contacts: 3, // Contact list (NIP-02)
EncryptedDM: 4, // Encrypted DM (NIP-04)
EventDeletion: 5, // Delete events (NIP-09)
Repost: 6, // Repost (NIP-18)
Reaction: 7, // Reaction (NIP-25)
ChannelCreation: 40, // Channel (NIP-28)
ChannelMessage: 42, // Channel message
Zap: 9735, // Zap receipt (NIP-57)
Report: 1984, // Report (NIP-56)
RelayList: 10002, // Relay list (NIP-65)
Article: 30023, // Long-form content (NIP-23)
};
```
### Creating Specific Events
```javascript
// Profile metadata (kind 0)
const profileEvent = finalizeEvent({
kind: 0,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: JSON.stringify({
name: 'Alice',
about: 'Nostr enthusiast',
picture: 'https://example.com/avatar.jpg',
nip05: 'alice@example.com',
lud16: 'alice@getalby.com'
})
}, secretKey);
// Contact list (kind 3)
const contactsEvent = finalizeEvent({
kind: 3,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', pubkey1, 'wss://relay1.com', 'alice'],
['p', pubkey2, 'wss://relay2.com', 'bob'],
['p', pubkey3, '', 'carol']
],
content: '' // Or JSON relay preferences
}, secretKey);
// Reply to an event
const replyEvent = finalizeEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
['e', rootEventId, '', 'root'],
['e', parentEventId, '', 'reply'],
['p', parentEventPubkey]
],
content: 'This is a reply'
}, secretKey);
// Reaction (kind 7)
const reactionEvent = finalizeEvent({
kind: 7,
created_at: Math.floor(Date.now() / 1000),
tags: [
['e', eventId],
['p', eventPubkey]
],
content: '+' // or '-' or emoji
}, secretKey);
// Delete event (kind 5)
const deleteEvent = finalizeEvent({
kind: 5,
created_at: Math.floor(Date.now() / 1000),
tags: [
['e', eventIdToDelete],
['e', anotherEventIdToDelete]
],
content: 'Deletion reason'
}, secretKey);
```
## Relay Communication
### Using SimplePool
SimplePool is the recommended way to interact with multiple relays:
```javascript
import { SimplePool } from 'nostr-tools/pool';
const pool = new SimplePool();
const relays = [
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.nostr.band'
];
// Subscribe to events
const subscription = pool.subscribeMany(
relays,
[
{
kinds: [1],
authors: [publicKey],
limit: 10
}
],
{
onevent(event) {
console.log('Received event:', event);
},
oneose() {
console.log('End of stored events');
}
}
);
// Close subscription when done
subscription.close();
// Publish event to all relays
const results = await Promise.allSettled(
pool.publish(relays, signedEvent)
);
// Query events (returns Promise)
const events = await pool.querySync(relays, {
kinds: [0],
authors: [publicKey]
});
// Get single event
const event = await pool.get(relays, {
ids: [eventId]
});
// Close pool when done
pool.close(relays);
```
### Direct Relay Connection
```javascript
import { Relay } from 'nostr-tools/relay';
const relay = await Relay.connect('wss://relay.damus.io');
console.log(`Connected to ${relay.url}`);
// Subscribe
const sub = relay.subscribe([
{
kinds: [1],
limit: 100
}
], {
onevent(event) {
console.log('Event:', event);
},
oneose() {
console.log('EOSE');
sub.close();
}
});
// Publish
await relay.publish(signedEvent);
// Close
relay.close();
```
### Handling Connection States
```javascript
import { Relay } from 'nostr-tools/relay';
const relay = await Relay.connect('wss://relay.example.com');
// Listen for disconnect
relay.onclose = () => {
console.log('Relay disconnected');
};
// Check connection status
console.log('Connected:', relay.connected);
```
## Filters
### Filter Structure
```javascript
const filter = {
// Event IDs
ids: ['abc123...'],
// Authors (pubkeys)
authors: ['pubkey1', 'pubkey2'],
// Event kinds
kinds: [1, 6, 7],
// Tags (single-letter keys)
'#e': ['eventId1', 'eventId2'],
'#p': ['pubkey1'],
'#t': ['nostr', 'bitcoin'],
'#d': ['article-identifier'],
// Time range
since: 1704067200, // Unix timestamp
until: 1704153600,
// Limit results
limit: 100,
// Search (NIP-50, if relay supports)
search: 'nostr protocol'
};
```
### Common Filter Patterns
```javascript
// User's recent posts
const userPosts = {
kinds: [1],
authors: [userPubkey],
limit: 50
};
// User's profile
const userProfile = {
kinds: [0],
authors: [userPubkey]
};
// User's contacts
const userContacts = {
kinds: [3],
authors: [userPubkey]
};
// Replies to an event
const replies = {
kinds: [1],
'#e': [eventId]
};
// Reactions to an event
const reactions = {
kinds: [7],
'#e': [eventId]
};
// Feed from followed users
const feed = {
kinds: [1, 6],
authors: followedPubkeys,
limit: 100
};
// Events mentioning user
const mentions = {
kinds: [1],
'#p': [userPubkey],
limit: 50
};
// Hashtag search
const hashtagEvents = {
kinds: [1],
'#t': ['bitcoin'],
limit: 100
};
// Replaceable event by d-tag
const replaceableEvent = {
kinds: [30023],
authors: [authorPubkey],
'#d': ['article-slug']
};
```
### Multiple Filters
```javascript
// Subscribe with multiple filters (OR logic)
const filters = [
{ kinds: [1], authors: [userPubkey], limit: 20 },
{ kinds: [1], '#p': [userPubkey], limit: 20 }
];
pool.subscribeMany(relays, filters, {
onevent(event) {
// Receives events matching ANY filter
}
});
```
## Encryption
### NIP-04 (Legacy DMs)
```javascript
import { nip04 } from 'nostr-tools';
// Encrypt message
const ciphertext = await nip04.encrypt(
secretKey,
recipientPubkey,
'Hello, this is secret!'
);
// Create encrypted DM event
const dmEvent = finalizeEvent({
kind: 4,
created_at: Math.floor(Date.now() / 1000),
tags: [['p', recipientPubkey]],
content: ciphertext
}, secretKey);
// Decrypt message
const plaintext = await nip04.decrypt(
secretKey,
senderPubkey,
ciphertext
);
```
### NIP-44 (Modern Encryption)
```javascript
import { nip44 } from 'nostr-tools';
// Get conversation key (cache this for multiple messages)
const conversationKey = nip44.getConversationKey(
secretKey,
recipientPubkey
);
// Encrypt
const ciphertext = nip44.encrypt(
'Hello with NIP-44!',
conversationKey
);
// Decrypt
const plaintext = nip44.decrypt(
ciphertext,
conversationKey
);
```
## NIP Implementations
### NIP-05 (DNS Identifier)
```javascript
import { nip05 } from 'nostr-tools';
// Query NIP-05 identifier
const profile = await nip05.queryProfile('alice@example.com');
if (profile) {
console.log('Pubkey:', profile.pubkey);
console.log('Relays:', profile.relays);
}
// Verify NIP-05 for a pubkey
const isValid = await nip05.queryProfile('alice@example.com')
.then(p => p?.pubkey === expectedPubkey);
```
### NIP-10 (Reply Threading)
```javascript
import { nip10 } from 'nostr-tools';
// Parse reply tags
const parsed = nip10.parse(event);
console.log('Root:', parsed.root); // Original event
console.log('Reply:', parsed.reply); // Direct parent
console.log('Mentions:', parsed.mentions); // Other mentions
console.log('Profiles:', parsed.profiles); // Mentioned pubkeys
```
### NIP-21 (nostr: URIs)
```javascript
// Parse nostr: URIs
const uri = 'nostr:npub1...';
const { type, data } = nip19.decode(uri.replace('nostr:', ''));
```
### NIP-27 (Content References)
```javascript
// Parse nostr:npub and nostr:note references in content
const content = 'Check out nostr:npub1abc... and nostr:note1xyz...';
const references = content.match(/nostr:(n[a-z]+1[a-z0-9]+)/g);
references?.forEach(ref => {
const decoded = nip19.decode(ref.replace('nostr:', ''));
console.log(decoded.type, decoded.data);
});
```
### NIP-57 (Zaps)
```javascript
import { nip57 } from 'nostr-tools';
// Validate zap receipt
const zapReceipt = await pool.get(relays, {
kinds: [9735],
'#e': [eventId]
});
const validatedZap = await nip57.validateZapRequest(zapReceipt);
```
## Utilities
### Hex and Bytes Conversion
```javascript
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
// Convert secret key to hex
const secretKeyHex = bytesToHex(secretKey);
// Convert hex back to bytes
const secretKeyBytes = hexToBytes(secretKeyHex);
```
### Event ID Calculation
```javascript
import { getEventHash } from 'nostr-tools/pure';
// Calculate event ID without signing
const eventId = getEventHash(unsignedEvent);
```
### Signature Operations
```javascript
import {
getSignature,
verifyEvent
} from 'nostr-tools/pure';
// Sign event data
const signature = getSignature(unsignedEvent, secretKey);
// Verify complete event
const isValid = verifyEvent(signedEvent);
```
## Best Practices
### Connection Management
1. **Use SimplePool** - Manages connections efficiently
2. **Limit concurrent connections** - Don't connect to too many relays
3. **Handle disconnections** - Implement reconnection logic
4. **Close subscriptions** - Always close when done
### Event Handling
1. **Verify events** - Always verify signatures
2. **Deduplicate** - Events may come from multiple relays
3. **Handle replaceable events** - Latest by created_at wins
4. **Validate content** - Don't trust event content blindly
### Key Security
1. **Never expose secret keys** - Keep in secure storage
2. **Use NIP-07 in browsers** - Let extensions handle signing
3. **Validate input** - Check key formats before use
### Performance
1. **Cache events** - Avoid re-fetching
2. **Use filters wisely** - Be specific, use limits
3. **Batch operations** - Combine related queries
4. **Close idle connections** - Free up resources
## Common Patterns
### Building a Feed
```javascript
const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
async function loadFeed(followedPubkeys) {
const events = await pool.querySync(relays, {
kinds: [1, 6],
authors: followedPubkeys,
limit: 100
});
// Sort by timestamp
return events.sort((a, b) => b.created_at - a.created_at);
}
```
### Real-time Updates
```javascript
function subscribeToFeed(followedPubkeys, onEvent) {
return pool.subscribeMany(
relays,
[{ kinds: [1, 6], authors: followedPubkeys }],
{
onevent: onEvent,
oneose() {
console.log('Caught up with stored events');
}
}
);
}
```
### Profile Loading
```javascript
async function loadProfile(pubkey) {
const [metadata] = await pool.querySync(relays, {
kinds: [0],
authors: [pubkey],
limit: 1
});
if (metadata) {
return JSON.parse(metadata.content);
}
return null;
}
```
### Event Deduplication
```javascript
const seenEvents = new Set();
function handleEvent(event) {
if (seenEvents.has(event.id)) {
return; // Skip duplicate
}
seenEvents.add(event.id);
// Process event...
}
```
## Troubleshooting
### Common Issues
**Events not publishing:**
- Check relay is writable
- Verify event is properly signed
- Check relay's accepted kinds
**Subscription not receiving events:**
- Verify filter syntax
- Check relay has matching events
- Ensure subscription isn't closed
**Signature verification fails:**
- Check event structure is correct
- Verify keys are in correct format
- Ensure event hasn't been modified
**NIP-05 lookup fails:**
- Check CORS headers on server
- Verify .well-known path is correct
- Handle network timeouts
## References
- **nostr-tools GitHub**: https://github.com/nbd-wtf/nostr-tools
- **Nostr Protocol**: https://github.com/nostr-protocol/nostr
- **NIPs Repository**: https://github.com/nostr-protocol/nips
- **NIP-01 (Basic Protocol)**: https://github.com/nostr-protocol/nips/blob/master/01.md
## Related Skills
- **nostr** - Nostr protocol fundamentals
- **svelte** - Building Nostr UIs with Svelte
- **applesauce-core** - Higher-level Nostr client utilities
- **applesauce-signers** - Nostr signing abstractions

View File

@@ -0,0 +1,899 @@
---
name: rollup
description: This skill should be used when working with Rollup module bundler, including configuration, plugins, code splitting, and build optimization. Provides comprehensive knowledge of Rollup patterns, plugin development, and bundling strategies.
---
# Rollup Skill
This skill provides comprehensive knowledge and patterns for working with Rollup module bundler effectively.
## When to Use This Skill
Use this skill when:
- Configuring Rollup for web applications
- Setting up Rollup for library builds
- Working with Rollup plugins
- Implementing code splitting
- Optimizing bundle size
- Troubleshooting build issues
- Integrating Rollup with Svelte or other frameworks
- Developing custom Rollup plugins
## Core Concepts
### Rollup Overview
Rollup is a module bundler that:
- **Tree-shakes by default** - Removes unused code automatically
- **ES module focused** - Native ESM output support
- **Plugin-based** - Extensible architecture
- **Multiple outputs** - Generate multiple formats from single input
- **Code splitting** - Dynamic imports for lazy loading
- **Scope hoisting** - Flattens modules for smaller bundles
### Basic Configuration
```javascript
// rollup.config.js
export default {
input: 'src/main.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
}
};
```
### Output Formats
Rollup supports multiple output formats:
| Format | Description | Use Case |
|--------|-------------|----------|
| `esm` | ES modules | Modern browsers, bundlers |
| `cjs` | CommonJS | Node.js |
| `iife` | Self-executing function | Script tags |
| `umd` | Universal Module Definition | CDN, both environments |
| `amd` | Asynchronous Module Definition | RequireJS |
| `system` | SystemJS | SystemJS loader |
## Configuration
### Full Configuration Options
```javascript
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
const production = !process.env.ROLLUP_WATCH;
export default {
// Entry point(s)
input: 'src/main.js',
// Output configuration
output: {
// Output file or directory
file: 'dist/bundle.js',
// Or for code splitting:
// dir: 'dist',
// Output format
format: 'esm',
// Name for IIFE/UMD builds
name: 'MyBundle',
// Sourcemap generation
sourcemap: true,
// Global variables for external imports (IIFE/UMD)
globals: {
jquery: '$'
},
// Banner/footer comments
banner: '/* My library v1.0.0 */',
footer: '/* End of bundle */',
// Chunk naming for code splitting
chunkFileNames: '[name]-[hash].js',
entryFileNames: '[name].js',
// Manual chunks for code splitting
manualChunks: {
vendor: ['lodash', 'moment']
},
// Interop mode for default exports
interop: 'auto',
// Preserve modules structure
preserveModules: false,
// Exports mode
exports: 'auto' // 'default', 'named', 'none', 'auto'
},
// External dependencies (not bundled)
external: ['lodash', /^node:/],
// Plugin array
plugins: [
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
production && terser()
],
// Watch mode options
watch: {
include: 'src/**',
exclude: 'node_modules/**',
clearScreen: false
},
// Warning handling
onwarn(warning, warn) {
// Skip certain warnings
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
warn(warning);
},
// Preserve entry signatures for code splitting
preserveEntrySignatures: 'strict',
// Treeshake options
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false
}
};
```
### Multiple Outputs
```javascript
export default {
input: 'src/main.js',
output: [
{
file: 'dist/bundle.esm.js',
format: 'esm'
},
{
file: 'dist/bundle.cjs.js',
format: 'cjs'
},
{
file: 'dist/bundle.umd.js',
format: 'umd',
name: 'MyLibrary'
}
]
};
```
### Multiple Entry Points
```javascript
export default {
input: {
main: 'src/main.js',
utils: 'src/utils.js'
},
output: {
dir: 'dist',
format: 'esm'
}
};
```
### Array of Configurations
```javascript
export default [
{
input: 'src/main.js',
output: { file: 'dist/main.js', format: 'esm' }
},
{
input: 'src/worker.js',
output: { file: 'dist/worker.js', format: 'iife' }
}
];
```
## Essential Plugins
### @rollup/plugin-node-resolve
Resolve node_modules imports:
```javascript
import resolve from '@rollup/plugin-node-resolve';
export default {
plugins: [
resolve({
// Resolve browser field in package.json
browser: true,
// Prefer built-in modules
preferBuiltins: true,
// Only resolve these extensions
extensions: ['.mjs', '.js', '.json', '.node'],
// Dedupe packages (important for Svelte)
dedupe: ['svelte'],
// Main fields to check in package.json
mainFields: ['module', 'main', 'browser'],
// Export conditions
exportConditions: ['svelte', 'browser', 'module', 'import']
})
]
};
```
### @rollup/plugin-commonjs
Convert CommonJS to ES modules:
```javascript
import commonjs from '@rollup/plugin-commonjs';
export default {
plugins: [
commonjs({
// Include specific modules
include: /node_modules/,
// Exclude specific modules
exclude: ['node_modules/lodash-es/**'],
// Ignore conditional requires
ignoreDynamicRequires: false,
// Transform mixed ES/CJS modules
transformMixedEsModules: true,
// Named exports for specific modules
namedExports: {
'react': ['createElement', 'Component']
}
})
]
};
```
### @rollup/plugin-terser
Minify output:
```javascript
import terser from '@rollup/plugin-terser';
export default {
plugins: [
terser({
compress: {
drop_console: true,
drop_debugger: true
},
mangle: true,
format: {
comments: false
}
})
]
};
```
### rollup-plugin-svelte
Compile Svelte components:
```javascript
import svelte from 'rollup-plugin-svelte';
import css from 'rollup-plugin-css-only';
export default {
plugins: [
svelte({
// Enable dev mode
dev: !production,
// Emit CSS as a separate file
emitCss: true,
// Preprocess (SCSS, TypeScript, etc.)
preprocess: sveltePreprocess(),
// Compiler options
compilerOptions: {
dev: !production
},
// Custom element mode
customElement: false
}),
// Extract CSS to separate file
css({ output: 'bundle.css' })
]
};
```
### Other Common Plugins
```javascript
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import alias from '@rollup/plugin-alias';
import image from '@rollup/plugin-image';
import copy from 'rollup-plugin-copy';
import livereload from 'rollup-plugin-livereload';
export default {
plugins: [
// Import JSON files
json(),
// Replace strings in code
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production'),
'__VERSION__': JSON.stringify('1.0.0')
}),
// Path aliases
alias({
entries: [
{ find: '@', replacement: './src' },
{ find: 'utils', replacement: './src/utils' }
]
}),
// Import images
image(),
// Copy static files
copy({
targets: [
{ src: 'public/*', dest: 'dist' }
]
}),
// Live reload in dev
!production && livereload('dist')
]
};
```
## Code Splitting
### Dynamic Imports
```javascript
// Automatically creates chunks
async function loadFeature() {
const { feature } = await import('./feature.js');
feature();
}
```
Configuration for code splitting:
```javascript
export default {
input: 'src/main.js',
output: {
dir: 'dist',
format: 'esm',
chunkFileNames: 'chunks/[name]-[hash].js'
}
};
```
### Manual Chunks
```javascript
export default {
output: {
manualChunks: {
// Vendor chunk
vendor: ['lodash', 'moment'],
// Or use a function for more control
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
}
};
```
### Advanced Chunking Strategy
```javascript
export default {
output: {
manualChunks(id, { getModuleInfo }) {
// Separate chunks by feature
if (id.includes('/features/auth/')) {
return 'auth';
}
if (id.includes('/features/dashboard/')) {
return 'dashboard';
}
// Vendor chunks by package
if (id.includes('node_modules')) {
const match = id.match(/node_modules\/([^/]+)/);
if (match) {
const packageName = match[1];
// Group small packages
const smallPackages = ['lodash', 'date-fns'];
if (smallPackages.includes(packageName)) {
return 'vendor-utils';
}
return `vendor-${packageName}`;
}
}
}
}
};
```
## Watch Mode
### Configuration
```javascript
export default {
watch: {
// Files to watch
include: 'src/**',
// Files to ignore
exclude: 'node_modules/**',
// Don't clear screen on rebuild
clearScreen: false,
// Rebuild delay
buildDelay: 0,
// Watch chokidar options
chokidar: {
usePolling: true
}
}
};
```
### CLI Watch Mode
```bash
# Watch mode
rollup -c -w
# With environment variable
ROLLUP_WATCH=true rollup -c
```
## Plugin Development
### Plugin Structure
```javascript
function myPlugin(options = {}) {
return {
// Plugin name (required)
name: 'my-plugin',
// Build hooks
options(inputOptions) {
// Modify input options
return inputOptions;
},
buildStart(inputOptions) {
// Called on build start
},
resolveId(source, importer, options) {
// Custom module resolution
if (source === 'virtual-module') {
return source;
}
return null; // Defer to other plugins
},
load(id) {
// Load module content
if (id === 'virtual-module') {
return 'export default "Hello"';
}
return null;
},
transform(code, id) {
// Transform module code
if (id.endsWith('.txt')) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null
};
}
},
buildEnd(error) {
// Called when build ends
if (error) {
console.error('Build failed:', error);
}
},
// Output generation hooks
renderStart(outputOptions, inputOptions) {
// Called before output generation
},
banner() {
return '/* Custom banner */';
},
footer() {
return '/* Custom footer */';
},
renderChunk(code, chunk, options) {
// Transform output chunk
return code;
},
generateBundle(options, bundle) {
// Modify output bundle
for (const fileName in bundle) {
const chunk = bundle[fileName];
if (chunk.type === 'chunk') {
// Modify chunk
}
}
},
writeBundle(options, bundle) {
// After bundle is written
},
closeBundle() {
// Called when bundle is closed
}
};
}
export default myPlugin;
```
### Plugin with Rollup Utils
```javascript
import { createFilter } from '@rollup/pluginutils';
function myTransformPlugin(options = {}) {
const filter = createFilter(options.include, options.exclude);
return {
name: 'my-transform',
transform(code, id) {
if (!filter(id)) return null;
// Transform code
const transformed = code.replace(/foo/g, 'bar');
return {
code: transformed,
map: null // Or generate sourcemap
};
}
};
}
```
## Svelte Integration
### Complete Svelte Setup
```javascript
// rollup.config.js
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import css from 'rollup-plugin-css-only';
import livereload from 'rollup-plugin-livereload';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn(
'npm',
['run', 'start', '--', '--dev'],
{
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
}
);
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
compilerOptions: {
dev: !production
}
}),
css({ output: 'bundle.css' }),
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// Dev server
!production && serve(),
!production && livereload('public'),
// Minify in production
production && terser()
],
watch: {
clearScreen: false
}
};
```
## Best Practices
### Bundle Optimization
1. **Enable tree shaking** - Use ES modules
2. **Mark side effects** - Set `sideEffects` in package.json
3. **Use terser** - Minify production builds
4. **Analyze bundles** - Use rollup-plugin-visualizer
5. **Code split** - Lazy load routes and features
### External Dependencies
```javascript
export default {
// Don't bundle peer dependencies for libraries
external: [
'react',
'react-dom',
/^lodash\//
],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
};
```
### Development vs Production
```javascript
const production = !process.env.ROLLUP_WATCH;
export default {
plugins: [
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify(
production ? 'production' : 'development'
)
}),
production && terser()
].filter(Boolean)
};
```
### Error Handling
```javascript
export default {
onwarn(warning, warn) {
// Ignore circular dependency warnings
if (warning.code === 'CIRCULAR_DEPENDENCY') {
return;
}
// Ignore unused external imports
if (warning.code === 'UNUSED_EXTERNAL_IMPORT') {
return;
}
// Treat other warnings as errors
if (warning.code === 'UNRESOLVED_IMPORT') {
throw new Error(warning.message);
}
// Use default warning handling
warn(warning);
}
};
```
## Common Patterns
### Library Build
```javascript
import pkg from './package.json';
export default {
input: 'src/index.js',
external: Object.keys(pkg.peerDependencies || {}),
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true
},
{
file: pkg.module,
format: 'esm',
sourcemap: true
}
]
};
```
### Application Build
```javascript
export default {
input: 'src/main.js',
output: {
dir: 'dist',
format: 'esm',
chunkFileNames: 'chunks/[name]-[hash].js',
entryFileNames: '[name]-[hash].js',
sourcemap: true
},
plugins: [
// All dependencies bundled
resolve({ browser: true }),
commonjs(),
terser()
]
};
```
### Web Worker Build
```javascript
export default [
// Main application
{
input: 'src/main.js',
output: {
file: 'dist/main.js',
format: 'esm'
},
plugins: [resolve(), commonjs()]
},
// Web worker (IIFE format)
{
input: 'src/worker.js',
output: {
file: 'dist/worker.js',
format: 'iife'
},
plugins: [resolve(), commonjs()]
}
];
```
## Troubleshooting
### Common Issues
**Module not found:**
- Check @rollup/plugin-node-resolve is configured
- Verify package is installed
- Check `external` array
**CommonJS module issues:**
- Add @rollup/plugin-commonjs
- Check `namedExports` configuration
- Try `transformMixedEsModules: true`
**Circular dependencies:**
- Use `onwarn` to suppress or fix
- Refactor to break cycles
- Check import order
**Sourcemaps not working:**
- Set `sourcemap: true` in output
- Ensure plugins pass through maps
- Check browser devtools settings
**Large bundle size:**
- Use rollup-plugin-visualizer
- Check for duplicate dependencies
- Verify tree shaking is working
- Mark unused packages as external
## CLI Reference
```bash
# Basic build
rollup -c
# Watch mode
rollup -c -w
# Custom config
rollup -c rollup.custom.config.js
# Output format
rollup src/main.js --format esm --file dist/bundle.js
# Environment variables
NODE_ENV=production rollup -c
# Silent mode
rollup -c --silent
# Generate bundle stats
rollup -c --perf
```
## References
- **Rollup Documentation**: https://rollupjs.org
- **Plugin Directory**: https://github.com/rollup/plugins
- **Awesome Rollup**: https://github.com/rollup/awesome
- **GitHub**: https://github.com/rollup/rollup
## Related Skills
- **svelte** - Using Rollup with Svelte
- **typescript** - TypeScript compilation with Rollup
- **nostr-tools** - Bundling Nostr applications

File diff suppressed because it is too large Load Diff