Browse Source
- 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>main
7 changed files with 4066 additions and 1 deletions
@ -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 |
||||||
@ -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 |
||||||
@ -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 |
||||||
@ -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
Loading…
Reference in new issue