Add Claude Code skills for web frontend frameworks
- Add Svelte 3/4 skill covering components, reactivity, stores, lifecycle - Add Rollup skill covering configuration, plugins, code splitting - Add nostr-tools skill covering event creation, signing, relay communication - Add applesauce-core skill covering event stores, reactive queries - Add applesauce-signers skill covering NIP-07/NIP-46 signing abstractions - Update .gitignore to include .claude/** directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,10 @@
|
|||||||
"Bash(./scripts/test.sh:*)",
|
"Bash(./scripts/test.sh:*)",
|
||||||
"Bash(./scripts/update-embedded-web.sh:*)",
|
"Bash(./scripts/update-embedded-web.sh:*)",
|
||||||
"Bash(bun run build:*)",
|
"Bash(bun run build:*)",
|
||||||
"Bash(bun update:*)"
|
"Bash(bun update:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
634
.claude/skills/applesauce-core/SKILL.md
Normal file
634
.claude/skills/applesauce-core/SKILL.md
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
---
|
||||||
|
name: applesauce-core
|
||||||
|
description: This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications.
|
||||||
|
---
|
||||||
|
|
||||||
|
# applesauce-core Skill
|
||||||
|
|
||||||
|
This skill provides comprehensive knowledge and patterns for working with applesauce-core, a library that provides reactive utilities and patterns for building Nostr clients.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
- Building reactive Nostr applications
|
||||||
|
- Managing event stores and caches
|
||||||
|
- Working with observable patterns for Nostr
|
||||||
|
- Implementing real-time updates
|
||||||
|
- Building timeline and feed views
|
||||||
|
- Managing replaceable events
|
||||||
|
- Working with profiles and metadata
|
||||||
|
- Creating efficient Nostr queries
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### applesauce-core Overview
|
||||||
|
|
||||||
|
applesauce-core provides:
|
||||||
|
- **Event stores** - Reactive event caching and management
|
||||||
|
- **Queries** - Declarative event querying patterns
|
||||||
|
- **Observables** - RxJS-based reactive patterns
|
||||||
|
- **Profile helpers** - Profile metadata management
|
||||||
|
- **Timeline utilities** - Feed and timeline building
|
||||||
|
- **NIP helpers** - NIP-specific utilities
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install applesauce-core
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Architecture
|
||||||
|
|
||||||
|
applesauce-core is built on reactive principles:
|
||||||
|
- Events are stored in reactive stores
|
||||||
|
- Queries return observables that update when new events arrive
|
||||||
|
- Components subscribe to observables for real-time updates
|
||||||
|
|
||||||
|
## Event Store
|
||||||
|
|
||||||
|
### Creating an Event Store
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { EventStore } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Create event store
|
||||||
|
const eventStore = new EventStore();
|
||||||
|
|
||||||
|
// Add events
|
||||||
|
eventStore.add(event1);
|
||||||
|
eventStore.add(event2);
|
||||||
|
|
||||||
|
// Add multiple events
|
||||||
|
eventStore.addMany([event1, event2, event3]);
|
||||||
|
|
||||||
|
// Check if event exists
|
||||||
|
const exists = eventStore.has(eventId);
|
||||||
|
|
||||||
|
// Get event by ID
|
||||||
|
const event = eventStore.get(eventId);
|
||||||
|
|
||||||
|
// Remove event
|
||||||
|
eventStore.remove(eventId);
|
||||||
|
|
||||||
|
// Clear all events
|
||||||
|
eventStore.clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Store Queries
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get all events
|
||||||
|
const allEvents = eventStore.getAll();
|
||||||
|
|
||||||
|
// Get events by filter
|
||||||
|
const filtered = eventStore.filter({
|
||||||
|
kinds: [1],
|
||||||
|
authors: [pubkey]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get events by author
|
||||||
|
const authorEvents = eventStore.getByAuthor(pubkey);
|
||||||
|
|
||||||
|
// Get events by kind
|
||||||
|
const textNotes = eventStore.getByKind(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replaceable Events
|
||||||
|
|
||||||
|
applesauce-core handles replaceable events automatically:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// For kind 0 (profile), only latest is kept
|
||||||
|
eventStore.add(profileEvent1); // stored
|
||||||
|
eventStore.add(profileEvent2); // replaces if newer
|
||||||
|
|
||||||
|
// For parameterized replaceable (30000-39999)
|
||||||
|
eventStore.add(articleEvent); // keyed by author + kind + d-tag
|
||||||
|
|
||||||
|
// Get replaceable event
|
||||||
|
const profile = eventStore.getReplaceable(0, pubkey);
|
||||||
|
const article = eventStore.getReplaceable(30023, pubkey, 'article-slug');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Queries
|
||||||
|
|
||||||
|
### Query Patterns
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createQuery } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Create a query
|
||||||
|
const query = createQuery(eventStore, {
|
||||||
|
kinds: [1],
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to query results
|
||||||
|
query.subscribe(events => {
|
||||||
|
console.log('Current events:', events);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query updates automatically when new events added
|
||||||
|
eventStore.add(newEvent); // Subscribers notified
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeline Query
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { TimelineQuery } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Create timeline for user's notes
|
||||||
|
const timeline = new TimelineQuery(eventStore, {
|
||||||
|
kinds: [1],
|
||||||
|
authors: [userPubkey]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get observable of timeline
|
||||||
|
const timeline$ = timeline.events$;
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
timeline$.subscribe(events => {
|
||||||
|
// Events sorted by created_at, newest first
|
||||||
|
renderTimeline(events);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Profile Query
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ProfileQuery } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Query profile metadata
|
||||||
|
const profileQuery = new ProfileQuery(eventStore, pubkey);
|
||||||
|
|
||||||
|
// Get observable
|
||||||
|
const profile$ = profileQuery.profile$;
|
||||||
|
|
||||||
|
profile$.subscribe(profile => {
|
||||||
|
if (profile) {
|
||||||
|
console.log('Name:', profile.name);
|
||||||
|
console.log('Picture:', profile.picture);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observables
|
||||||
|
|
||||||
|
### Working with RxJS
|
||||||
|
|
||||||
|
applesauce-core uses RxJS observables:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { map, filter, distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
|
||||||
|
// Transform query results
|
||||||
|
const names$ = profileQuery.profile$.pipe(
|
||||||
|
filter(profile => profile !== null),
|
||||||
|
map(profile => profile.name),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine multiple observables
|
||||||
|
import { combineLatest } from 'rxjs';
|
||||||
|
|
||||||
|
const combined$ = combineLatest([
|
||||||
|
timeline$,
|
||||||
|
profile$
|
||||||
|
]).pipe(
|
||||||
|
map(([events, profile]) => ({
|
||||||
|
events,
|
||||||
|
authorName: profile?.name
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Custom Observables
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
function createEventObservable(store, filter) {
|
||||||
|
return new Observable(subscriber => {
|
||||||
|
// Initial emit
|
||||||
|
subscriber.next(store.filter(filter));
|
||||||
|
|
||||||
|
// Subscribe to store changes
|
||||||
|
const unsubscribe = store.onChange(() => {
|
||||||
|
subscriber.next(store.filter(filter));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Profile Helpers
|
||||||
|
|
||||||
|
### Profile Metadata
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { parseProfile, ProfileContent } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Parse kind 0 content
|
||||||
|
const profileEvent = await getProfileEvent(pubkey);
|
||||||
|
const profile = parseProfile(profileEvent);
|
||||||
|
|
||||||
|
// Profile fields
|
||||||
|
console.log(profile.name); // Display name
|
||||||
|
console.log(profile.about); // Bio
|
||||||
|
console.log(profile.picture); // Avatar URL
|
||||||
|
console.log(profile.banner); // Banner image URL
|
||||||
|
console.log(profile.nip05); // NIP-05 identifier
|
||||||
|
console.log(profile.lud16); // Lightning address
|
||||||
|
console.log(profile.website); // Website URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Profile Store
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ProfileStore } from 'applesauce-core';
|
||||||
|
|
||||||
|
const profileStore = new ProfileStore(eventStore);
|
||||||
|
|
||||||
|
// Get profile observable
|
||||||
|
const profile$ = profileStore.getProfile(pubkey);
|
||||||
|
|
||||||
|
// Get multiple profiles
|
||||||
|
const profiles$ = profileStore.getProfiles([pubkey1, pubkey2]);
|
||||||
|
|
||||||
|
// Request profile load (triggers fetch if not cached)
|
||||||
|
profileStore.requestProfile(pubkey);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timeline Utilities
|
||||||
|
|
||||||
|
### Building Feeds
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Timeline } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Create timeline
|
||||||
|
const timeline = new Timeline(eventStore);
|
||||||
|
|
||||||
|
// Add filter
|
||||||
|
timeline.setFilter({
|
||||||
|
kinds: [1, 6],
|
||||||
|
authors: followedPubkeys
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get events observable
|
||||||
|
const events$ = timeline.events$;
|
||||||
|
|
||||||
|
// Load more (pagination)
|
||||||
|
timeline.loadMore(50);
|
||||||
|
|
||||||
|
// Refresh (get latest)
|
||||||
|
timeline.refresh();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Thread Building
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ThreadBuilder } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Build thread from root event
|
||||||
|
const thread = new ThreadBuilder(eventStore, rootEventId);
|
||||||
|
|
||||||
|
// Get thread observable
|
||||||
|
const thread$ = thread.thread$;
|
||||||
|
|
||||||
|
thread$.subscribe(threadData => {
|
||||||
|
console.log('Root:', threadData.root);
|
||||||
|
console.log('Replies:', threadData.replies);
|
||||||
|
console.log('Reply count:', threadData.replyCount);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reactions and Zaps
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ReactionStore, ZapStore } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Reactions
|
||||||
|
const reactionStore = new ReactionStore(eventStore);
|
||||||
|
const reactions$ = reactionStore.getReactions(eventId);
|
||||||
|
|
||||||
|
reactions$.subscribe(reactions => {
|
||||||
|
console.log('Likes:', reactions.likes);
|
||||||
|
console.log('Custom:', reactions.custom);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zaps
|
||||||
|
const zapStore = new ZapStore(eventStore);
|
||||||
|
const zaps$ = zapStore.getZaps(eventId);
|
||||||
|
|
||||||
|
zaps$.subscribe(zaps => {
|
||||||
|
console.log('Total sats:', zaps.totalAmount);
|
||||||
|
console.log('Zap count:', zaps.count);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## NIP Helpers
|
||||||
|
|
||||||
|
### NIP-05 Verification
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { verifyNip05 } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Verify NIP-05
|
||||||
|
const result = await verifyNip05('alice@example.com', expectedPubkey);
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
console.log('NIP-05 verified');
|
||||||
|
} else {
|
||||||
|
console.log('Verification failed:', result.error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-10 Reply Parsing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { parseReplyTags } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Parse reply structure
|
||||||
|
const parsed = parseReplyTags(event);
|
||||||
|
|
||||||
|
console.log('Root event:', parsed.root);
|
||||||
|
console.log('Reply to:', parsed.reply);
|
||||||
|
console.log('Mentions:', parsed.mentions);
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-65 Relay Lists
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { parseRelayList } from 'applesauce-core';
|
||||||
|
|
||||||
|
// Parse relay list event (kind 10002)
|
||||||
|
const relays = parseRelayList(relayListEvent);
|
||||||
|
|
||||||
|
console.log('Read relays:', relays.read);
|
||||||
|
console.log('Write relays:', relays.write);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with nostr-tools
|
||||||
|
|
||||||
|
### Using with SimplePool
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { SimplePool } from 'nostr-tools';
|
||||||
|
import { EventStore } from 'applesauce-core';
|
||||||
|
|
||||||
|
const pool = new SimplePool();
|
||||||
|
const eventStore = new EventStore();
|
||||||
|
|
||||||
|
// Load events into store
|
||||||
|
pool.subscribeMany(relays, [filter], {
|
||||||
|
onevent(event) {
|
||||||
|
eventStore.add(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query store reactively
|
||||||
|
const timeline$ = createTimelineQuery(eventStore, filter);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publishing Events
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { finalizeEvent } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Create event
|
||||||
|
const event = finalizeEvent({
|
||||||
|
kind: 1,
|
||||||
|
content: 'Hello!',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: []
|
||||||
|
}, secretKey);
|
||||||
|
|
||||||
|
// Add to local store immediately (optimistic update)
|
||||||
|
eventStore.add(event);
|
||||||
|
|
||||||
|
// Publish to relays
|
||||||
|
await pool.publish(relays, event);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Svelte Integration
|
||||||
|
|
||||||
|
### Using in Svelte Components
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { EventStore, TimelineQuery } from 'applesauce-core';
|
||||||
|
|
||||||
|
export let pubkey;
|
||||||
|
|
||||||
|
const eventStore = new EventStore();
|
||||||
|
let events = [];
|
||||||
|
let subscription;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const timeline = new TimelineQuery(eventStore, {
|
||||||
|
kinds: [1],
|
||||||
|
authors: [pubkey]
|
||||||
|
});
|
||||||
|
|
||||||
|
subscription = timeline.events$.subscribe(e => {
|
||||||
|
events = e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
subscription?.unsubscribe();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each events as event}
|
||||||
|
<div class="event">
|
||||||
|
{event.content}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Svelte Store Adapter
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { readable } from 'svelte/store';
|
||||||
|
|
||||||
|
// Convert RxJS observable to Svelte store
|
||||||
|
function fromObservable(observable, initialValue) {
|
||||||
|
return readable(initialValue, set => {
|
||||||
|
const subscription = observable.subscribe(set);
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const events$ = timeline.events$;
|
||||||
|
const eventsStore = fromObservable(events$, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { eventsStore } from './stores.js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each $eventsStore as event}
|
||||||
|
<div>{event.content}</div>
|
||||||
|
{/each}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Store Management
|
||||||
|
|
||||||
|
1. **Single store instance** - Use one EventStore per app
|
||||||
|
2. **Clear stale data** - Implement cache limits
|
||||||
|
3. **Handle replaceable events** - Let store manage deduplication
|
||||||
|
4. **Unsubscribe** - Clean up subscriptions on component destroy
|
||||||
|
|
||||||
|
### Query Optimization
|
||||||
|
|
||||||
|
1. **Use specific filters** - Narrow queries perform better
|
||||||
|
2. **Limit results** - Use limit for initial loads
|
||||||
|
3. **Cache queries** - Reuse query instances
|
||||||
|
4. **Debounce updates** - Throttle rapid changes
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
|
||||||
|
1. **Limit store size** - Implement LRU or time-based eviction
|
||||||
|
2. **Clean up observables** - Unsubscribe when done
|
||||||
|
3. **Use weak references** - For profile caches
|
||||||
|
4. **Paginate large feeds** - Don't load everything at once
|
||||||
|
|
||||||
|
### Reactive Patterns
|
||||||
|
|
||||||
|
1. **Prefer observables** - Over imperative queries
|
||||||
|
2. **Use operators** - Transform data with RxJS
|
||||||
|
3. **Combine streams** - For complex views
|
||||||
|
4. **Handle loading states** - Show placeholders
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Event Deduplication
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// EventStore handles deduplication automatically
|
||||||
|
eventStore.add(event1);
|
||||||
|
eventStore.add(event1); // No duplicate
|
||||||
|
|
||||||
|
// For manual deduplication
|
||||||
|
const seen = new Set();
|
||||||
|
events.filter(e => {
|
||||||
|
if (seen.has(e.id)) return false;
|
||||||
|
seen.add(e.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimistic Updates
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function publishNote(content) {
|
||||||
|
// Create event
|
||||||
|
const event = await createEvent(content);
|
||||||
|
|
||||||
|
// Add to store immediately (optimistic)
|
||||||
|
eventStore.add(event);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Publish to relays
|
||||||
|
await pool.publish(relays, event);
|
||||||
|
} catch (error) {
|
||||||
|
// Remove on failure
|
||||||
|
eventStore.remove(event.id);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading States
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { BehaviorSubject, combineLatest } from 'rxjs';
|
||||||
|
|
||||||
|
const loading$ = new BehaviorSubject(true);
|
||||||
|
const events$ = timeline.events$;
|
||||||
|
|
||||||
|
const state$ = combineLatest([loading$, events$]).pipe(
|
||||||
|
map(([loading, events]) => ({
|
||||||
|
loading,
|
||||||
|
events,
|
||||||
|
empty: !loading && events.length === 0
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start loading
|
||||||
|
loading$.next(true);
|
||||||
|
await loadEvents();
|
||||||
|
loading$.next(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infinite Scroll
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function createInfiniteScroll(timeline, pageSize = 50) {
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
async function loadMore() {
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
await timeline.loadMore(pageSize);
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll(event) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = event.target;
|
||||||
|
if (scrollHeight - scrollTop <= clientHeight * 1.5) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loadMore, onScroll };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Events not updating:**
|
||||||
|
- Check subscription is active
|
||||||
|
- Verify events are being added to store
|
||||||
|
- Ensure filter matches events
|
||||||
|
|
||||||
|
**Memory growing:**
|
||||||
|
- Implement store size limits
|
||||||
|
- Clean up subscriptions
|
||||||
|
- Use weak references where appropriate
|
||||||
|
|
||||||
|
**Slow queries:**
|
||||||
|
- Add indexes for common queries
|
||||||
|
- Use more specific filters
|
||||||
|
- Implement pagination
|
||||||
|
|
||||||
|
**Stale data:**
|
||||||
|
- Implement refresh mechanisms
|
||||||
|
- Set up real-time subscriptions
|
||||||
|
- Handle replaceable event updates
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **applesauce GitHub**: https://github.com/hzrd149/applesauce
|
||||||
|
- **RxJS Documentation**: https://rxjs.dev
|
||||||
|
- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools
|
||||||
|
- **Nostr Protocol**: https://github.com/nostr-protocol/nostr
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- **nostr-tools** - Lower-level Nostr operations
|
||||||
|
- **applesauce-signers** - Event signing abstractions
|
||||||
|
- **svelte** - Building reactive UIs
|
||||||
|
- **nostr** - Nostr protocol fundamentals
|
||||||
757
.claude/skills/applesauce-signers/SKILL.md
Normal file
757
.claude/skills/applesauce-signers/SKILL.md
Normal file
@@ -0,0 +1,757 @@
|
|||||||
|
---
|
||||||
|
name: applesauce-signers
|
||||||
|
description: This skill should be used when working with applesauce-signers library for Nostr event signing, including NIP-07 browser extensions, NIP-46 remote signing, and custom signer implementations. Provides comprehensive knowledge of signing patterns and signer abstractions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# applesauce-signers Skill
|
||||||
|
|
||||||
|
This skill provides comprehensive knowledge and patterns for working with applesauce-signers, a library that provides signing abstractions for Nostr applications.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
- Implementing event signing in Nostr applications
|
||||||
|
- Integrating with NIP-07 browser extensions
|
||||||
|
- Working with NIP-46 remote signers
|
||||||
|
- Building custom signer implementations
|
||||||
|
- Managing signing sessions
|
||||||
|
- Handling signing requests and permissions
|
||||||
|
- Implementing multi-signer support
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### applesauce-signers Overview
|
||||||
|
|
||||||
|
applesauce-signers provides:
|
||||||
|
- **Signer abstraction** - Unified interface for different signers
|
||||||
|
- **NIP-07 integration** - Browser extension support
|
||||||
|
- **NIP-46 support** - Remote signing (Nostr Connect)
|
||||||
|
- **Simple signers** - Direct key signing
|
||||||
|
- **Permission handling** - Manage signing requests
|
||||||
|
- **Observable patterns** - Reactive signing states
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install applesauce-signers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signer Interface
|
||||||
|
|
||||||
|
All signers implement a common interface:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Signer {
|
||||||
|
// Get public key
|
||||||
|
getPublicKey(): Promise<string>;
|
||||||
|
|
||||||
|
// Sign event
|
||||||
|
signEvent(event: UnsignedEvent): Promise<SignedEvent>;
|
||||||
|
|
||||||
|
// Encrypt (NIP-04)
|
||||||
|
nip04Encrypt?(pubkey: string, plaintext: string): Promise<string>;
|
||||||
|
nip04Decrypt?(pubkey: string, ciphertext: string): Promise<string>;
|
||||||
|
|
||||||
|
// Encrypt (NIP-44)
|
||||||
|
nip44Encrypt?(pubkey: string, plaintext: string): Promise<string>;
|
||||||
|
nip44Decrypt?(pubkey: string, ciphertext: string): Promise<string>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simple Signer
|
||||||
|
|
||||||
|
### Using Secret Key
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { SimpleSigner } from 'applesauce-signers';
|
||||||
|
import { generateSecretKey } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Create signer with existing key
|
||||||
|
const signer = new SimpleSigner(secretKey);
|
||||||
|
|
||||||
|
// Or generate new key
|
||||||
|
const newSecretKey = generateSecretKey();
|
||||||
|
const newSigner = new SimpleSigner(newSecretKey);
|
||||||
|
|
||||||
|
// Get public key
|
||||||
|
const pubkey = await signer.getPublicKey();
|
||||||
|
|
||||||
|
// Sign event
|
||||||
|
const unsignedEvent = {
|
||||||
|
kind: 1,
|
||||||
|
content: 'Hello Nostr!',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const signedEvent = await signer.signEvent(unsignedEvent);
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-04 Encryption
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Encrypt message
|
||||||
|
const ciphertext = await signer.nip04Encrypt(
|
||||||
|
recipientPubkey,
|
||||||
|
'Secret message'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decrypt message
|
||||||
|
const plaintext = await signer.nip04Decrypt(
|
||||||
|
senderPubkey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-44 Encryption
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Encrypt with NIP-44 (preferred)
|
||||||
|
const ciphertext = await signer.nip44Encrypt(
|
||||||
|
recipientPubkey,
|
||||||
|
'Secret message'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
const plaintext = await signer.nip44Decrypt(
|
||||||
|
senderPubkey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## NIP-07 Signer
|
||||||
|
|
||||||
|
### Browser Extension Integration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Nip07Signer } from 'applesauce-signers';
|
||||||
|
|
||||||
|
// Check if extension is available
|
||||||
|
if (window.nostr) {
|
||||||
|
const signer = new Nip07Signer();
|
||||||
|
|
||||||
|
// Get public key (may prompt user)
|
||||||
|
const pubkey = await signer.getPublicKey();
|
||||||
|
|
||||||
|
// Sign event (prompts user)
|
||||||
|
const signedEvent = await signer.signEvent(unsignedEvent);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Extension Availability
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function getAvailableSigner() {
|
||||||
|
if (typeof window !== 'undefined' && window.nostr) {
|
||||||
|
return new Nip07Signer();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for extension to load
|
||||||
|
async function waitForExtension(timeout = 3000) {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
if (window.nostr) {
|
||||||
|
return new Nip07Signer();
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extension Permissions
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Some extensions support granular permissions
|
||||||
|
const signer = new Nip07Signer();
|
||||||
|
|
||||||
|
// Request specific permissions
|
||||||
|
try {
|
||||||
|
// This varies by extension
|
||||||
|
await window.nostr.enable();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('User denied permission');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## NIP-46 Remote Signer
|
||||||
|
|
||||||
|
### Nostr Connect
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Nip46Signer } from 'applesauce-signers';
|
||||||
|
|
||||||
|
// Create remote signer
|
||||||
|
const signer = new Nip46Signer({
|
||||||
|
// Remote signer's pubkey
|
||||||
|
remotePubkey: signerPubkey,
|
||||||
|
|
||||||
|
// Relays for communication
|
||||||
|
relays: ['wss://relay.example.com'],
|
||||||
|
|
||||||
|
// Local secret key for encryption
|
||||||
|
localSecretKey: localSecretKey,
|
||||||
|
|
||||||
|
// Optional: custom client name
|
||||||
|
clientName: 'My Nostr App'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to remote signer
|
||||||
|
await signer.connect();
|
||||||
|
|
||||||
|
// Get public key
|
||||||
|
const pubkey = await signer.getPublicKey();
|
||||||
|
|
||||||
|
// Sign event
|
||||||
|
const signedEvent = await signer.signEvent(unsignedEvent);
|
||||||
|
|
||||||
|
// Disconnect when done
|
||||||
|
signer.disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection URL
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Parse nostrconnect:// URL
|
||||||
|
function parseNostrConnectUrl(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pubkey: parsed.pathname.replace('//', ''),
|
||||||
|
relay: parsed.searchParams.get('relay'),
|
||||||
|
secret: parsed.searchParams.get('secret')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create signer from URL
|
||||||
|
const { pubkey, relay, secret } = parseNostrConnectUrl(connectUrl);
|
||||||
|
|
||||||
|
const signer = new Nip46Signer({
|
||||||
|
remotePubkey: pubkey,
|
||||||
|
relays: [relay],
|
||||||
|
localSecretKey: generateSecretKey(),
|
||||||
|
secret: secret
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bunker URL
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Parse bunker:// URL (NIP-46)
|
||||||
|
function parseBunkerUrl(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pubkey: parsed.pathname.replace('//', ''),
|
||||||
|
relays: parsed.searchParams.getAll('relay'),
|
||||||
|
secret: parsed.searchParams.get('secret')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pubkey, relays, secret } = parseBunkerUrl(bunkerUrl);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signer Management
|
||||||
|
|
||||||
|
### Signer Store
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { SignerStore } from 'applesauce-signers';
|
||||||
|
|
||||||
|
const signerStore = new SignerStore();
|
||||||
|
|
||||||
|
// Set active signer
|
||||||
|
signerStore.setSigner(signer);
|
||||||
|
|
||||||
|
// Get active signer
|
||||||
|
const activeSigner = signerStore.getSigner();
|
||||||
|
|
||||||
|
// Clear signer (logout)
|
||||||
|
signerStore.clearSigner();
|
||||||
|
|
||||||
|
// Observable for signer changes
|
||||||
|
signerStore.signer$.subscribe(signer => {
|
||||||
|
if (signer) {
|
||||||
|
console.log('Logged in');
|
||||||
|
} else {
|
||||||
|
console.log('Logged out');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Account Support
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class AccountManager {
|
||||||
|
constructor() {
|
||||||
|
this.accounts = new Map();
|
||||||
|
this.activeAccount = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAccount(pubkey, signer) {
|
||||||
|
this.accounts.set(pubkey, signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAccount(pubkey) {
|
||||||
|
this.accounts.delete(pubkey);
|
||||||
|
if (this.activeAccount === pubkey) {
|
||||||
|
this.activeAccount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchAccount(pubkey) {
|
||||||
|
if (this.accounts.has(pubkey)) {
|
||||||
|
this.activeAccount = pubkey;
|
||||||
|
return this.accounts.get(pubkey);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveSigner() {
|
||||||
|
return this.activeAccount
|
||||||
|
? this.accounts.get(this.activeAccount)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Signers
|
||||||
|
|
||||||
|
### Implementing a Custom Signer
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class CustomSigner {
|
||||||
|
constructor(options) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicKey() {
|
||||||
|
// Return public key
|
||||||
|
return this.options.pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signEvent(event) {
|
||||||
|
// Implement signing logic
|
||||||
|
// Could call external API, hardware wallet, etc.
|
||||||
|
|
||||||
|
const signedEvent = await this.externalSign(event);
|
||||||
|
return signedEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip04Encrypt(pubkey, plaintext) {
|
||||||
|
// Implement NIP-04 encryption
|
||||||
|
throw new Error('NIP-04 not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip04Decrypt(pubkey, ciphertext) {
|
||||||
|
throw new Error('NIP-04 not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip44Encrypt(pubkey, plaintext) {
|
||||||
|
// Implement NIP-44 encryption
|
||||||
|
throw new Error('NIP-44 not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip44Decrypt(pubkey, ciphertext) {
|
||||||
|
throw new Error('NIP-44 not supported');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hardware Wallet Signer
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class HardwareWalletSigner {
|
||||||
|
constructor(devicePath) {
|
||||||
|
this.devicePath = devicePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
// Connect to hardware device
|
||||||
|
this.device = await connectToDevice(this.devicePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicKey() {
|
||||||
|
// Get public key from device
|
||||||
|
return await this.device.getNostrPubkey();
|
||||||
|
}
|
||||||
|
|
||||||
|
async signEvent(event) {
|
||||||
|
// Sign on device (user confirms on device)
|
||||||
|
const signature = await this.device.signNostrEvent(event);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
pubkey: await this.getPublicKey(),
|
||||||
|
id: getEventHash(event),
|
||||||
|
sig: signature
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read-Only Signer
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ReadOnlySigner {
|
||||||
|
constructor(pubkey) {
|
||||||
|
this.pubkey = pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicKey() {
|
||||||
|
return this.pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signEvent(event) {
|
||||||
|
throw new Error('Read-only mode: cannot sign events');
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip04Encrypt(pubkey, plaintext) {
|
||||||
|
throw new Error('Read-only mode: cannot encrypt');
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip04Decrypt(pubkey, ciphertext) {
|
||||||
|
throw new Error('Read-only mode: cannot decrypt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signing Utilities
|
||||||
|
|
||||||
|
### Event Creation Helper
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function createAndSignEvent(signer, template) {
|
||||||
|
const pubkey = await signer.getPublicKey();
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
...template,
|
||||||
|
pubkey,
|
||||||
|
created_at: template.created_at || Math.floor(Date.now() / 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
return await signer.signEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const signedNote = await createAndSignEvent(signer, {
|
||||||
|
kind: 1,
|
||||||
|
content: 'Hello!',
|
||||||
|
tags: []
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Signing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function signEvents(signer, events) {
|
||||||
|
const signed = [];
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
const signedEvent = await signer.signEvent(event);
|
||||||
|
signed.push(signedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With parallelization (if signer supports)
|
||||||
|
async function signEventsParallel(signer, events) {
|
||||||
|
return Promise.all(
|
||||||
|
events.map(event => signer.signEvent(event))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Svelte Integration
|
||||||
|
|
||||||
|
### Signer Context
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- SignerProvider.svelte -->
|
||||||
|
<script>
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
const signer = writable(null);
|
||||||
|
|
||||||
|
setContext('signer', {
|
||||||
|
signer,
|
||||||
|
setSigner: (s) => signer.set(s),
|
||||||
|
clearSigner: () => signer.set(null)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
```
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- Component using signer -->
|
||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
const { signer } = getContext('signer');
|
||||||
|
|
||||||
|
async function publishNote(content) {
|
||||||
|
if (!$signer) {
|
||||||
|
alert('Please login first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = await $signer.signEvent({
|
||||||
|
kind: 1,
|
||||||
|
content,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Publish event...
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login Component
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { Nip07Signer, SimpleSigner } from 'applesauce-signers';
|
||||||
|
|
||||||
|
const { setSigner, clearSigner, signer } = getContext('signer');
|
||||||
|
|
||||||
|
let nsec = '';
|
||||||
|
|
||||||
|
async function loginWithExtension() {
|
||||||
|
if (window.nostr) {
|
||||||
|
setSigner(new Nip07Signer());
|
||||||
|
} else {
|
||||||
|
alert('No extension found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginWithNsec() {
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(nsec);
|
||||||
|
if (decoded.type === 'nsec') {
|
||||||
|
setSigner(new SimpleSigner(decoded.data));
|
||||||
|
nsec = '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Invalid nsec');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
clearSigner();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $signer}
|
||||||
|
<button on:click={logout}>Logout</button>
|
||||||
|
{:else}
|
||||||
|
<button on:click={loginWithExtension}>
|
||||||
|
Login with Extension
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
bind:value={nsec}
|
||||||
|
placeholder="nsec..."
|
||||||
|
/>
|
||||||
|
<button on:click={loginWithNsec}>
|
||||||
|
Login with Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
1. **Never store secret keys in plain text** - Use secure storage
|
||||||
|
2. **Prefer NIP-07** - Let extensions manage keys
|
||||||
|
3. **Clear keys on logout** - Don't leave in memory
|
||||||
|
4. **Validate before signing** - Check event content
|
||||||
|
|
||||||
|
### User Experience
|
||||||
|
|
||||||
|
1. **Show signing status** - Loading states
|
||||||
|
2. **Handle rejections gracefully** - User may cancel
|
||||||
|
3. **Provide fallbacks** - Multiple login options
|
||||||
|
4. **Remember preferences** - Store signer type
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function safeSign(signer, event) {
|
||||||
|
try {
|
||||||
|
return await signer.signEvent(event);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('rejected')) {
|
||||||
|
console.log('User rejected signing');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (error.message.includes('timeout')) {
|
||||||
|
console.log('Signing timed out');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Checking
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function hasEncryptionSupport(signer) {
|
||||||
|
return typeof signer.nip04Encrypt === 'function' ||
|
||||||
|
typeof signer.nip44Encrypt === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEncryptionMethod(signer) {
|
||||||
|
// Prefer NIP-44
|
||||||
|
if (typeof signer.nip44Encrypt === 'function') {
|
||||||
|
return 'nip44';
|
||||||
|
}
|
||||||
|
if (typeof signer.nip04Encrypt === 'function') {
|
||||||
|
return 'nip04';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Signer Detection
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function detectSigners() {
|
||||||
|
const available = [];
|
||||||
|
|
||||||
|
// Check NIP-07
|
||||||
|
if (typeof window !== 'undefined' && window.nostr) {
|
||||||
|
available.push({
|
||||||
|
type: 'nip07',
|
||||||
|
name: 'Browser Extension',
|
||||||
|
create: () => new Nip07Signer()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check stored credentials
|
||||||
|
const storedKey = localStorage.getItem('nsec');
|
||||||
|
if (storedKey) {
|
||||||
|
available.push({
|
||||||
|
type: 'stored',
|
||||||
|
name: 'Saved Key',
|
||||||
|
create: () => new SimpleSigner(storedKey)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Reconnect for NIP-46
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ReconnectingNip46Signer {
|
||||||
|
constructor(options) {
|
||||||
|
this.options = options;
|
||||||
|
this.signer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
this.signer = new Nip46Signer(this.options);
|
||||||
|
await this.signer.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async signEvent(event) {
|
||||||
|
try {
|
||||||
|
return await this.signer.signEvent(event);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('disconnected')) {
|
||||||
|
await this.connect();
|
||||||
|
return await this.signer.signEvent(event);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signer Type Persistence
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const SIGNER_KEY = 'nostr_signer_type';
|
||||||
|
|
||||||
|
function saveSigner(type, data) {
|
||||||
|
localStorage.setItem(SIGNER_KEY, JSON.stringify({ type, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreSigner() {
|
||||||
|
const saved = localStorage.getItem(SIGNER_KEY);
|
||||||
|
if (!saved) return null;
|
||||||
|
|
||||||
|
const { type, data } = JSON.parse(saved);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'nip07':
|
||||||
|
if (window.nostr) {
|
||||||
|
return new Nip07Signer();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'simple':
|
||||||
|
// Don't store secret keys!
|
||||||
|
break;
|
||||||
|
case 'nip46':
|
||||||
|
const signer = new Nip46Signer(data);
|
||||||
|
await signer.connect();
|
||||||
|
return signer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Extension not detected:**
|
||||||
|
- Wait for page load
|
||||||
|
- Check window.nostr exists
|
||||||
|
- Verify extension is enabled
|
||||||
|
|
||||||
|
**Signing rejected:**
|
||||||
|
- User cancelled in extension
|
||||||
|
- Handle gracefully with error message
|
||||||
|
|
||||||
|
**NIP-46 connection fails:**
|
||||||
|
- Check relay is accessible
|
||||||
|
- Verify remote signer is online
|
||||||
|
- Check secret matches
|
||||||
|
|
||||||
|
**Encryption not supported:**
|
||||||
|
- Check signer has encrypt methods
|
||||||
|
- Fall back to alternative method
|
||||||
|
- Show user appropriate error
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **applesauce GitHub**: https://github.com/hzrd149/applesauce
|
||||||
|
- **NIP-07 Specification**: https://github.com/nostr-protocol/nips/blob/master/07.md
|
||||||
|
- **NIP-46 Specification**: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||||
|
- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- **nostr-tools** - Event creation and signing utilities
|
||||||
|
- **applesauce-core** - Event stores and queries
|
||||||
|
- **nostr** - Nostr protocol fundamentals
|
||||||
|
- **svelte** - Building Nostr UIs
|
||||||
767
.claude/skills/nostr-tools/SKILL.md
Normal file
767
.claude/skills/nostr-tools/SKILL.md
Normal file
@@ -0,0 +1,767 @@
|
|||||||
|
---
|
||||||
|
name: nostr-tools
|
||||||
|
description: This skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns.
|
||||||
|
---
|
||||||
|
|
||||||
|
# nostr-tools Skill
|
||||||
|
|
||||||
|
This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
- Building Nostr clients or applications
|
||||||
|
- Creating and signing Nostr events
|
||||||
|
- Connecting to Nostr relays
|
||||||
|
- Implementing NIP features
|
||||||
|
- Working with Nostr keys and cryptography
|
||||||
|
- Filtering and querying events
|
||||||
|
- Building relay pools or connections
|
||||||
|
- Implementing NIP-44/NIP-04 encryption
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### nostr-tools Overview
|
||||||
|
|
||||||
|
nostr-tools provides:
|
||||||
|
- **Event handling** - Create, sign, verify events
|
||||||
|
- **Key management** - Generate, convert, encode keys
|
||||||
|
- **Relay communication** - Connect, subscribe, publish
|
||||||
|
- **NIP implementations** - NIP-04, NIP-05, NIP-19, NIP-44, etc.
|
||||||
|
- **Cryptographic operations** - Schnorr signatures, encryption
|
||||||
|
- **Filter building** - Query events by various criteria
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install nostr-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Imports
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Core functionality
|
||||||
|
import {
|
||||||
|
SimplePool,
|
||||||
|
generateSecretKey,
|
||||||
|
getPublicKey,
|
||||||
|
finalizeEvent,
|
||||||
|
verifyEvent
|
||||||
|
} from 'nostr-tools';
|
||||||
|
|
||||||
|
// NIP-specific imports
|
||||||
|
import { nip04, nip05, nip19, nip44 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Relay operations
|
||||||
|
import { Relay } from 'nostr-tools/relay';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Management
|
||||||
|
|
||||||
|
### Generating Keys
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
||||||
|
|
||||||
|
// Generate new secret key (Uint8Array)
|
||||||
|
const secretKey = generateSecretKey();
|
||||||
|
|
||||||
|
// Derive public key
|
||||||
|
const publicKey = getPublicKey(secretKey);
|
||||||
|
|
||||||
|
console.log('Secret key:', bytesToHex(secretKey));
|
||||||
|
console.log('Public key:', publicKey); // hex string
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Encoding (NIP-19)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Encode to bech32
|
||||||
|
const nsec = nip19.nsecEncode(secretKey);
|
||||||
|
const npub = nip19.npubEncode(publicKey);
|
||||||
|
const note = nip19.noteEncode(eventId);
|
||||||
|
|
||||||
|
console.log(nsec); // nsec1...
|
||||||
|
console.log(npub); // npub1...
|
||||||
|
console.log(note); // note1...
|
||||||
|
|
||||||
|
// Decode from bech32
|
||||||
|
const { type, data } = nip19.decode(npub);
|
||||||
|
// type: 'npub', data: publicKey (hex)
|
||||||
|
|
||||||
|
// Encode profile reference (nprofile)
|
||||||
|
const nprofile = nip19.nprofileEncode({
|
||||||
|
pubkey: publicKey,
|
||||||
|
relays: ['wss://relay.example.com']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encode event reference (nevent)
|
||||||
|
const nevent = nip19.neventEncode({
|
||||||
|
id: eventId,
|
||||||
|
relays: ['wss://relay.example.com'],
|
||||||
|
author: publicKey,
|
||||||
|
kind: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Encode address (naddr) for replaceable events
|
||||||
|
const naddr = nip19.naddrEncode({
|
||||||
|
identifier: 'my-article',
|
||||||
|
pubkey: publicKey,
|
||||||
|
kind: 30023,
|
||||||
|
relays: ['wss://relay.example.com']
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Operations
|
||||||
|
|
||||||
|
### Event Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Unsigned event template
|
||||||
|
const eventTemplate = {
|
||||||
|
kind: 1,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello Nostr!'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Signed event (after finalizeEvent)
|
||||||
|
const signedEvent = {
|
||||||
|
id: '...', // 32-byte sha256 hash as hex
|
||||||
|
pubkey: '...', // 32-byte public key as hex
|
||||||
|
created_at: 1234567890,
|
||||||
|
kind: 1,
|
||||||
|
tags: [],
|
||||||
|
content: 'Hello Nostr!',
|
||||||
|
sig: '...' // 64-byte Schnorr signature as hex
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating and Signing Events
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure';
|
||||||
|
|
||||||
|
// Create event template
|
||||||
|
const eventTemplate = {
|
||||||
|
kind: 1,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['p', publicKey], // Mention
|
||||||
|
['e', eventId, '', 'reply'], // Reply
|
||||||
|
['t', 'nostr'] // Hashtag
|
||||||
|
],
|
||||||
|
content: 'Hello Nostr!'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign event
|
||||||
|
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
||||||
|
|
||||||
|
// Verify event
|
||||||
|
const isValid = verifyEvent(signedEvent);
|
||||||
|
console.log('Event valid:', isValid);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Kinds
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Common event kinds
|
||||||
|
const KINDS = {
|
||||||
|
Metadata: 0, // Profile metadata (NIP-01)
|
||||||
|
Text: 1, // Short text note (NIP-01)
|
||||||
|
RecommendRelay: 2, // Relay recommendation
|
||||||
|
Contacts: 3, // Contact list (NIP-02)
|
||||||
|
EncryptedDM: 4, // Encrypted DM (NIP-04)
|
||||||
|
EventDeletion: 5, // Delete events (NIP-09)
|
||||||
|
Repost: 6, // Repost (NIP-18)
|
||||||
|
Reaction: 7, // Reaction (NIP-25)
|
||||||
|
ChannelCreation: 40, // Channel (NIP-28)
|
||||||
|
ChannelMessage: 42, // Channel message
|
||||||
|
Zap: 9735, // Zap receipt (NIP-57)
|
||||||
|
Report: 1984, // Report (NIP-56)
|
||||||
|
RelayList: 10002, // Relay list (NIP-65)
|
||||||
|
Article: 30023, // Long-form content (NIP-23)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Specific Events
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Profile metadata (kind 0)
|
||||||
|
const profileEvent = finalizeEvent({
|
||||||
|
kind: 0,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
content: JSON.stringify({
|
||||||
|
name: 'Alice',
|
||||||
|
about: 'Nostr enthusiast',
|
||||||
|
picture: 'https://example.com/avatar.jpg',
|
||||||
|
nip05: 'alice@example.com',
|
||||||
|
lud16: 'alice@getalby.com'
|
||||||
|
})
|
||||||
|
}, secretKey);
|
||||||
|
|
||||||
|
// Contact list (kind 3)
|
||||||
|
const contactsEvent = finalizeEvent({
|
||||||
|
kind: 3,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['p', pubkey1, 'wss://relay1.com', 'alice'],
|
||||||
|
['p', pubkey2, 'wss://relay2.com', 'bob'],
|
||||||
|
['p', pubkey3, '', 'carol']
|
||||||
|
],
|
||||||
|
content: '' // Or JSON relay preferences
|
||||||
|
}, secretKey);
|
||||||
|
|
||||||
|
// Reply to an event
|
||||||
|
const replyEvent = finalizeEvent({
|
||||||
|
kind: 1,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['e', rootEventId, '', 'root'],
|
||||||
|
['e', parentEventId, '', 'reply'],
|
||||||
|
['p', parentEventPubkey]
|
||||||
|
],
|
||||||
|
content: 'This is a reply'
|
||||||
|
}, secretKey);
|
||||||
|
|
||||||
|
// Reaction (kind 7)
|
||||||
|
const reactionEvent = finalizeEvent({
|
||||||
|
kind: 7,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['e', eventId],
|
||||||
|
['p', eventPubkey]
|
||||||
|
],
|
||||||
|
content: '+' // or '-' or emoji
|
||||||
|
}, secretKey);
|
||||||
|
|
||||||
|
// Delete event (kind 5)
|
||||||
|
const deleteEvent = finalizeEvent({
|
||||||
|
kind: 5,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['e', eventIdToDelete],
|
||||||
|
['e', anotherEventIdToDelete]
|
||||||
|
],
|
||||||
|
content: 'Deletion reason'
|
||||||
|
}, secretKey);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relay Communication
|
||||||
|
|
||||||
|
### Using SimplePool
|
||||||
|
|
||||||
|
SimplePool is the recommended way to interact with multiple relays:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { SimplePool } from 'nostr-tools/pool';
|
||||||
|
|
||||||
|
const pool = new SimplePool();
|
||||||
|
const relays = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://nos.lol',
|
||||||
|
'wss://relay.nostr.band'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Subscribe to events
|
||||||
|
const subscription = pool.subscribeMany(
|
||||||
|
relays,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [1],
|
||||||
|
authors: [publicKey],
|
||||||
|
limit: 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
onevent(event) {
|
||||||
|
console.log('Received event:', event);
|
||||||
|
},
|
||||||
|
oneose() {
|
||||||
|
console.log('End of stored events');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close subscription when done
|
||||||
|
subscription.close();
|
||||||
|
|
||||||
|
// Publish event to all relays
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
pool.publish(relays, signedEvent)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query events (returns Promise)
|
||||||
|
const events = await pool.querySync(relays, {
|
||||||
|
kinds: [0],
|
||||||
|
authors: [publicKey]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get single event
|
||||||
|
const event = await pool.get(relays, {
|
||||||
|
ids: [eventId]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close pool when done
|
||||||
|
pool.close(relays);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Relay Connection
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Relay } from 'nostr-tools/relay';
|
||||||
|
|
||||||
|
const relay = await Relay.connect('wss://relay.damus.io');
|
||||||
|
|
||||||
|
console.log(`Connected to ${relay.url}`);
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
const sub = relay.subscribe([
|
||||||
|
{
|
||||||
|
kinds: [1],
|
||||||
|
limit: 100
|
||||||
|
}
|
||||||
|
], {
|
||||||
|
onevent(event) {
|
||||||
|
console.log('Event:', event);
|
||||||
|
},
|
||||||
|
oneose() {
|
||||||
|
console.log('EOSE');
|
||||||
|
sub.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Publish
|
||||||
|
await relay.publish(signedEvent);
|
||||||
|
|
||||||
|
// Close
|
||||||
|
relay.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling Connection States
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Relay } from 'nostr-tools/relay';
|
||||||
|
|
||||||
|
const relay = await Relay.connect('wss://relay.example.com');
|
||||||
|
|
||||||
|
// Listen for disconnect
|
||||||
|
relay.onclose = () => {
|
||||||
|
console.log('Relay disconnected');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check connection status
|
||||||
|
console.log('Connected:', relay.connected);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filters
|
||||||
|
|
||||||
|
### Filter Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const filter = {
|
||||||
|
// Event IDs
|
||||||
|
ids: ['abc123...'],
|
||||||
|
|
||||||
|
// Authors (pubkeys)
|
||||||
|
authors: ['pubkey1', 'pubkey2'],
|
||||||
|
|
||||||
|
// Event kinds
|
||||||
|
kinds: [1, 6, 7],
|
||||||
|
|
||||||
|
// Tags (single-letter keys)
|
||||||
|
'#e': ['eventId1', 'eventId2'],
|
||||||
|
'#p': ['pubkey1'],
|
||||||
|
'#t': ['nostr', 'bitcoin'],
|
||||||
|
'#d': ['article-identifier'],
|
||||||
|
|
||||||
|
// Time range
|
||||||
|
since: 1704067200, // Unix timestamp
|
||||||
|
until: 1704153600,
|
||||||
|
|
||||||
|
// Limit results
|
||||||
|
limit: 100,
|
||||||
|
|
||||||
|
// Search (NIP-50, if relay supports)
|
||||||
|
search: 'nostr protocol'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Filter Patterns
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// User's recent posts
|
||||||
|
const userPosts = {
|
||||||
|
kinds: [1],
|
||||||
|
authors: [userPubkey],
|
||||||
|
limit: 50
|
||||||
|
};
|
||||||
|
|
||||||
|
// User's profile
|
||||||
|
const userProfile = {
|
||||||
|
kinds: [0],
|
||||||
|
authors: [userPubkey]
|
||||||
|
};
|
||||||
|
|
||||||
|
// User's contacts
|
||||||
|
const userContacts = {
|
||||||
|
kinds: [3],
|
||||||
|
authors: [userPubkey]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replies to an event
|
||||||
|
const replies = {
|
||||||
|
kinds: [1],
|
||||||
|
'#e': [eventId]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reactions to an event
|
||||||
|
const reactions = {
|
||||||
|
kinds: [7],
|
||||||
|
'#e': [eventId]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Feed from followed users
|
||||||
|
const feed = {
|
||||||
|
kinds: [1, 6],
|
||||||
|
authors: followedPubkeys,
|
||||||
|
limit: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
// Events mentioning user
|
||||||
|
const mentions = {
|
||||||
|
kinds: [1],
|
||||||
|
'#p': [userPubkey],
|
||||||
|
limit: 50
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hashtag search
|
||||||
|
const hashtagEvents = {
|
||||||
|
kinds: [1],
|
||||||
|
'#t': ['bitcoin'],
|
||||||
|
limit: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replaceable event by d-tag
|
||||||
|
const replaceableEvent = {
|
||||||
|
kinds: [30023],
|
||||||
|
authors: [authorPubkey],
|
||||||
|
'#d': ['article-slug']
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Filters
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Subscribe with multiple filters (OR logic)
|
||||||
|
const filters = [
|
||||||
|
{ kinds: [1], authors: [userPubkey], limit: 20 },
|
||||||
|
{ kinds: [1], '#p': [userPubkey], limit: 20 }
|
||||||
|
];
|
||||||
|
|
||||||
|
pool.subscribeMany(relays, filters, {
|
||||||
|
onevent(event) {
|
||||||
|
// Receives events matching ANY filter
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Encryption
|
||||||
|
|
||||||
|
### NIP-04 (Legacy DMs)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { nip04 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Encrypt message
|
||||||
|
const ciphertext = await nip04.encrypt(
|
||||||
|
secretKey,
|
||||||
|
recipientPubkey,
|
||||||
|
'Hello, this is secret!'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create encrypted DM event
|
||||||
|
const dmEvent = finalizeEvent({
|
||||||
|
kind: 4,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', recipientPubkey]],
|
||||||
|
content: ciphertext
|
||||||
|
}, secretKey);
|
||||||
|
|
||||||
|
// Decrypt message
|
||||||
|
const plaintext = await nip04.decrypt(
|
||||||
|
secretKey,
|
||||||
|
senderPubkey,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-44 (Modern Encryption)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { nip44 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Get conversation key (cache this for multiple messages)
|
||||||
|
const conversationKey = nip44.getConversationKey(
|
||||||
|
secretKey,
|
||||||
|
recipientPubkey
|
||||||
|
);
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
const ciphertext = nip44.encrypt(
|
||||||
|
'Hello with NIP-44!',
|
||||||
|
conversationKey
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
const plaintext = nip44.decrypt(
|
||||||
|
ciphertext,
|
||||||
|
conversationKey
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## NIP Implementations
|
||||||
|
|
||||||
|
### NIP-05 (DNS Identifier)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { nip05 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Query NIP-05 identifier
|
||||||
|
const profile = await nip05.queryProfile('alice@example.com');
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
console.log('Pubkey:', profile.pubkey);
|
||||||
|
console.log('Relays:', profile.relays);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify NIP-05 for a pubkey
|
||||||
|
const isValid = await nip05.queryProfile('alice@example.com')
|
||||||
|
.then(p => p?.pubkey === expectedPubkey);
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-10 (Reply Threading)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { nip10 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Parse reply tags
|
||||||
|
const parsed = nip10.parse(event);
|
||||||
|
|
||||||
|
console.log('Root:', parsed.root); // Original event
|
||||||
|
console.log('Reply:', parsed.reply); // Direct parent
|
||||||
|
console.log('Mentions:', parsed.mentions); // Other mentions
|
||||||
|
console.log('Profiles:', parsed.profiles); // Mentioned pubkeys
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-21 (nostr: URIs)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Parse nostr: URIs
|
||||||
|
const uri = 'nostr:npub1...';
|
||||||
|
const { type, data } = nip19.decode(uri.replace('nostr:', ''));
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-27 (Content References)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Parse nostr:npub and nostr:note references in content
|
||||||
|
const content = 'Check out nostr:npub1abc... and nostr:note1xyz...';
|
||||||
|
|
||||||
|
const references = content.match(/nostr:(n[a-z]+1[a-z0-9]+)/g);
|
||||||
|
references?.forEach(ref => {
|
||||||
|
const decoded = nip19.decode(ref.replace('nostr:', ''));
|
||||||
|
console.log(decoded.type, decoded.data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### NIP-57 (Zaps)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { nip57 } from 'nostr-tools';
|
||||||
|
|
||||||
|
// Validate zap receipt
|
||||||
|
const zapReceipt = await pool.get(relays, {
|
||||||
|
kinds: [9735],
|
||||||
|
'#e': [eventId]
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedZap = await nip57.validateZapRequest(zapReceipt);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
### Hex and Bytes Conversion
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
||||||
|
|
||||||
|
// Convert secret key to hex
|
||||||
|
const secretKeyHex = bytesToHex(secretKey);
|
||||||
|
|
||||||
|
// Convert hex back to bytes
|
||||||
|
const secretKeyBytes = hexToBytes(secretKeyHex);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event ID Calculation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { getEventHash } from 'nostr-tools/pure';
|
||||||
|
|
||||||
|
// Calculate event ID without signing
|
||||||
|
const eventId = getEventHash(unsignedEvent);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signature Operations
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
getSignature,
|
||||||
|
verifyEvent
|
||||||
|
} from 'nostr-tools/pure';
|
||||||
|
|
||||||
|
// Sign event data
|
||||||
|
const signature = getSignature(unsignedEvent, secretKey);
|
||||||
|
|
||||||
|
// Verify complete event
|
||||||
|
const isValid = verifyEvent(signedEvent);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Connection Management
|
||||||
|
|
||||||
|
1. **Use SimplePool** - Manages connections efficiently
|
||||||
|
2. **Limit concurrent connections** - Don't connect to too many relays
|
||||||
|
3. **Handle disconnections** - Implement reconnection logic
|
||||||
|
4. **Close subscriptions** - Always close when done
|
||||||
|
|
||||||
|
### Event Handling
|
||||||
|
|
||||||
|
1. **Verify events** - Always verify signatures
|
||||||
|
2. **Deduplicate** - Events may come from multiple relays
|
||||||
|
3. **Handle replaceable events** - Latest by created_at wins
|
||||||
|
4. **Validate content** - Don't trust event content blindly
|
||||||
|
|
||||||
|
### Key Security
|
||||||
|
|
||||||
|
1. **Never expose secret keys** - Keep in secure storage
|
||||||
|
2. **Use NIP-07 in browsers** - Let extensions handle signing
|
||||||
|
3. **Validate input** - Check key formats before use
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
1. **Cache events** - Avoid re-fetching
|
||||||
|
2. **Use filters wisely** - Be specific, use limits
|
||||||
|
3. **Batch operations** - Combine related queries
|
||||||
|
4. **Close idle connections** - Free up resources
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Building a Feed
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const pool = new SimplePool();
|
||||||
|
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||||
|
|
||||||
|
async function loadFeed(followedPubkeys) {
|
||||||
|
const events = await pool.querySync(relays, {
|
||||||
|
kinds: [1, 6],
|
||||||
|
authors: followedPubkeys,
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by timestamp
|
||||||
|
return events.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-time Updates
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function subscribeToFeed(followedPubkeys, onEvent) {
|
||||||
|
return pool.subscribeMany(
|
||||||
|
relays,
|
||||||
|
[{ kinds: [1, 6], authors: followedPubkeys }],
|
||||||
|
{
|
||||||
|
onevent: onEvent,
|
||||||
|
oneose() {
|
||||||
|
console.log('Caught up with stored events');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Profile Loading
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function loadProfile(pubkey) {
|
||||||
|
const [metadata] = await pool.querySync(relays, {
|
||||||
|
kinds: [0],
|
||||||
|
authors: [pubkey],
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
return JSON.parse(metadata.content);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Deduplication
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const seenEvents = new Set();
|
||||||
|
|
||||||
|
function handleEvent(event) {
|
||||||
|
if (seenEvents.has(event.id)) {
|
||||||
|
return; // Skip duplicate
|
||||||
|
}
|
||||||
|
seenEvents.add(event.id);
|
||||||
|
|
||||||
|
// Process event...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Events not publishing:**
|
||||||
|
- Check relay is writable
|
||||||
|
- Verify event is properly signed
|
||||||
|
- Check relay's accepted kinds
|
||||||
|
|
||||||
|
**Subscription not receiving events:**
|
||||||
|
- Verify filter syntax
|
||||||
|
- Check relay has matching events
|
||||||
|
- Ensure subscription isn't closed
|
||||||
|
|
||||||
|
**Signature verification fails:**
|
||||||
|
- Check event structure is correct
|
||||||
|
- Verify keys are in correct format
|
||||||
|
- Ensure event hasn't been modified
|
||||||
|
|
||||||
|
**NIP-05 lookup fails:**
|
||||||
|
- Check CORS headers on server
|
||||||
|
- Verify .well-known path is correct
|
||||||
|
- Handle network timeouts
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **nostr-tools GitHub**: https://github.com/nbd-wtf/nostr-tools
|
||||||
|
- **Nostr Protocol**: https://github.com/nostr-protocol/nostr
|
||||||
|
- **NIPs Repository**: https://github.com/nostr-protocol/nips
|
||||||
|
- **NIP-01 (Basic Protocol)**: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- **nostr** - Nostr protocol fundamentals
|
||||||
|
- **svelte** - Building Nostr UIs with Svelte
|
||||||
|
- **applesauce-core** - Higher-level Nostr client utilities
|
||||||
|
- **applesauce-signers** - Nostr signing abstractions
|
||||||
899
.claude/skills/rollup/SKILL.md
Normal file
899
.claude/skills/rollup/SKILL.md
Normal file
@@ -0,0 +1,899 @@
|
|||||||
|
---
|
||||||
|
name: rollup
|
||||||
|
description: This skill should be used when working with Rollup module bundler, including configuration, plugins, code splitting, and build optimization. Provides comprehensive knowledge of Rollup patterns, plugin development, and bundling strategies.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rollup Skill
|
||||||
|
|
||||||
|
This skill provides comprehensive knowledge and patterns for working with Rollup module bundler effectively.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when:
|
||||||
|
- Configuring Rollup for web applications
|
||||||
|
- Setting up Rollup for library builds
|
||||||
|
- Working with Rollup plugins
|
||||||
|
- Implementing code splitting
|
||||||
|
- Optimizing bundle size
|
||||||
|
- Troubleshooting build issues
|
||||||
|
- Integrating Rollup with Svelte or other frameworks
|
||||||
|
- Developing custom Rollup plugins
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Rollup Overview
|
||||||
|
|
||||||
|
Rollup is a module bundler that:
|
||||||
|
- **Tree-shakes by default** - Removes unused code automatically
|
||||||
|
- **ES module focused** - Native ESM output support
|
||||||
|
- **Plugin-based** - Extensible architecture
|
||||||
|
- **Multiple outputs** - Generate multiple formats from single input
|
||||||
|
- **Code splitting** - Dynamic imports for lazy loading
|
||||||
|
- **Scope hoisting** - Flattens modules for smaller bundles
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// rollup.config.js
|
||||||
|
export default {
|
||||||
|
input: 'src/main.js',
|
||||||
|
output: {
|
||||||
|
file: 'dist/bundle.js',
|
||||||
|
format: 'esm'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Formats
|
||||||
|
|
||||||
|
Rollup supports multiple output formats:
|
||||||
|
|
||||||
|
| Format | Description | Use Case |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| `esm` | ES modules | Modern browsers, bundlers |
|
||||||
|
| `cjs` | CommonJS | Node.js |
|
||||||
|
| `iife` | Self-executing function | Script tags |
|
||||||
|
| `umd` | Universal Module Definition | CDN, both environments |
|
||||||
|
| `amd` | Asynchronous Module Definition | RequireJS |
|
||||||
|
| `system` | SystemJS | SystemJS loader |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Full Configuration Options
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// rollup.config.js
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import terser from '@rollup/plugin-terser';
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Entry point(s)
|
||||||
|
input: 'src/main.js',
|
||||||
|
|
||||||
|
// Output configuration
|
||||||
|
output: {
|
||||||
|
// Output file or directory
|
||||||
|
file: 'dist/bundle.js',
|
||||||
|
// Or for code splitting:
|
||||||
|
// dir: 'dist',
|
||||||
|
|
||||||
|
// Output format
|
||||||
|
format: 'esm',
|
||||||
|
|
||||||
|
// Name for IIFE/UMD builds
|
||||||
|
name: 'MyBundle',
|
||||||
|
|
||||||
|
// Sourcemap generation
|
||||||
|
sourcemap: true,
|
||||||
|
|
||||||
|
// Global variables for external imports (IIFE/UMD)
|
||||||
|
globals: {
|
||||||
|
jquery: '$'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Banner/footer comments
|
||||||
|
banner: '/* My library v1.0.0 */',
|
||||||
|
footer: '/* End of bundle */',
|
||||||
|
|
||||||
|
// Chunk naming for code splitting
|
||||||
|
chunkFileNames: '[name]-[hash].js',
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
|
||||||
|
// Manual chunks for code splitting
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['lodash', 'moment']
|
||||||
|
},
|
||||||
|
|
||||||
|
// Interop mode for default exports
|
||||||
|
interop: 'auto',
|
||||||
|
|
||||||
|
// Preserve modules structure
|
||||||
|
preserveModules: false,
|
||||||
|
|
||||||
|
// Exports mode
|
||||||
|
exports: 'auto' // 'default', 'named', 'none', 'auto'
|
||||||
|
},
|
||||||
|
|
||||||
|
// External dependencies (not bundled)
|
||||||
|
external: ['lodash', /^node:/],
|
||||||
|
|
||||||
|
// Plugin array
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
dedupe: ['svelte']
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
production && terser()
|
||||||
|
],
|
||||||
|
|
||||||
|
// Watch mode options
|
||||||
|
watch: {
|
||||||
|
include: 'src/**',
|
||||||
|
exclude: 'node_modules/**',
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
|
|
||||||
|
// Warning handling
|
||||||
|
onwarn(warning, warn) {
|
||||||
|
// Skip certain warnings
|
||||||
|
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
|
||||||
|
warn(warning);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Preserve entry signatures for code splitting
|
||||||
|
preserveEntrySignatures: 'strict',
|
||||||
|
|
||||||
|
// Treeshake options
|
||||||
|
treeshake: {
|
||||||
|
moduleSideEffects: false,
|
||||||
|
propertyReadSideEffects: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Outputs
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
input: 'src/main.js',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: 'dist/bundle.esm.js',
|
||||||
|
format: 'esm'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'dist/bundle.cjs.js',
|
||||||
|
format: 'cjs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'dist/bundle.umd.js',
|
||||||
|
format: 'umd',
|
||||||
|
name: 'MyLibrary'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Entry Points
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
input: {
|
||||||
|
main: 'src/main.js',
|
||||||
|
utils: 'src/utils.js'
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
dir: 'dist',
|
||||||
|
format: 'esm'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array of Configurations
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
input: 'src/main.js',
|
||||||
|
output: { file: 'dist/main.js', format: 'esm' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/worker.js',
|
||||||
|
output: { file: 'dist/worker.js', format: 'iife' }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Essential Plugins
|
||||||
|
|
||||||
|
### @rollup/plugin-node-resolve
|
||||||
|
|
||||||
|
Resolve node_modules imports:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [
|
||||||
|
resolve({
|
||||||
|
// Resolve browser field in package.json
|
||||||
|
browser: true,
|
||||||
|
|
||||||
|
// Prefer built-in modules
|
||||||
|
preferBuiltins: true,
|
||||||
|
|
||||||
|
// Only resolve these extensions
|
||||||
|
extensions: ['.mjs', '.js', '.json', '.node'],
|
||||||
|
|
||||||
|
// Dedupe packages (important for Svelte)
|
||||||
|
dedupe: ['svelte'],
|
||||||
|
|
||||||
|
// Main fields to check in package.json
|
||||||
|
mainFields: ['module', 'main', 'browser'],
|
||||||
|
|
||||||
|
// Export conditions
|
||||||
|
exportConditions: ['svelte', 'browser', 'module', 'import']
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### @rollup/plugin-commonjs
|
||||||
|
|
||||||
|
Convert CommonJS to ES modules:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [
|
||||||
|
commonjs({
|
||||||
|
// Include specific modules
|
||||||
|
include: /node_modules/,
|
||||||
|
|
||||||
|
// Exclude specific modules
|
||||||
|
exclude: ['node_modules/lodash-es/**'],
|
||||||
|
|
||||||
|
// Ignore conditional requires
|
||||||
|
ignoreDynamicRequires: false,
|
||||||
|
|
||||||
|
// Transform mixed ES/CJS modules
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
|
||||||
|
// Named exports for specific modules
|
||||||
|
namedExports: {
|
||||||
|
'react': ['createElement', 'Component']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### @rollup/plugin-terser
|
||||||
|
|
||||||
|
Minify output:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import terser from '@rollup/plugin-terser';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [
|
||||||
|
terser({
|
||||||
|
compress: {
|
||||||
|
drop_console: true,
|
||||||
|
drop_debugger: true
|
||||||
|
},
|
||||||
|
mangle: true,
|
||||||
|
format: {
|
||||||
|
comments: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### rollup-plugin-svelte
|
||||||
|
|
||||||
|
Compile Svelte components:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import svelte from 'rollup-plugin-svelte';
|
||||||
|
import css from 'rollup-plugin-css-only';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
// Enable dev mode
|
||||||
|
dev: !production,
|
||||||
|
|
||||||
|
// Emit CSS as a separate file
|
||||||
|
emitCss: true,
|
||||||
|
|
||||||
|
// Preprocess (SCSS, TypeScript, etc.)
|
||||||
|
preprocess: sveltePreprocess(),
|
||||||
|
|
||||||
|
// Compiler options
|
||||||
|
compilerOptions: {
|
||||||
|
dev: !production
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom element mode
|
||||||
|
customElement: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Extract CSS to separate file
|
||||||
|
css({ output: 'bundle.css' })
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Common Plugins
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import json from '@rollup/plugin-json';
|
||||||
|
import replace from '@rollup/plugin-replace';
|
||||||
|
import alias from '@rollup/plugin-alias';
|
||||||
|
import image from '@rollup/plugin-image';
|
||||||
|
import copy from 'rollup-plugin-copy';
|
||||||
|
import livereload from 'rollup-plugin-livereload';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [
|
||||||
|
// Import JSON files
|
||||||
|
json(),
|
||||||
|
|
||||||
|
// Replace strings in code
|
||||||
|
replace({
|
||||||
|
preventAssignment: true,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
|
'__VERSION__': JSON.stringify('1.0.0')
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Path aliases
|
||||||
|
alias({
|
||||||
|
entries: [
|
||||||
|
{ find: '@', replacement: './src' },
|
||||||
|
{ find: 'utils', replacement: './src/utils' }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Import images
|
||||||
|
image(),
|
||||||
|
|
||||||
|
// Copy static files
|
||||||
|
copy({
|
||||||
|
targets: [
|
||||||
|
{ src: 'public/*', dest: 'dist' }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Live reload in dev
|
||||||
|
!production && livereload('dist')
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Splitting
|
||||||
|
|
||||||
|
### Dynamic Imports
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Automatically creates chunks
|
||||||
|
async function loadFeature() {
|
||||||
|
const { feature } = await import('./feature.js');
|
||||||
|
feature();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration for code splitting:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
input: 'src/main.js',
|
||||||
|
output: {
|
||||||
|
dir: 'dist',
|
||||||
|
format: 'esm',
|
||||||
|
chunkFileNames: 'chunks/[name]-[hash].js'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Chunks
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
// Vendor chunk
|
||||||
|
vendor: ['lodash', 'moment'],
|
||||||
|
|
||||||
|
// Or use a function for more control
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return 'vendor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Chunking Strategy
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
output: {
|
||||||
|
manualChunks(id, { getModuleInfo }) {
|
||||||
|
// Separate chunks by feature
|
||||||
|
if (id.includes('/features/auth/')) {
|
||||||
|
return 'auth';
|
||||||
|
}
|
||||||
|
if (id.includes('/features/dashboard/')) {
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vendor chunks by package
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
const match = id.match(/node_modules\/([^/]+)/);
|
||||||
|
if (match) {
|
||||||
|
const packageName = match[1];
|
||||||
|
// Group small packages
|
||||||
|
const smallPackages = ['lodash', 'date-fns'];
|
||||||
|
if (smallPackages.includes(packageName)) {
|
||||||
|
return 'vendor-utils';
|
||||||
|
}
|
||||||
|
return `vendor-${packageName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Watch Mode
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
watch: {
|
||||||
|
// Files to watch
|
||||||
|
include: 'src/**',
|
||||||
|
|
||||||
|
// Files to ignore
|
||||||
|
exclude: 'node_modules/**',
|
||||||
|
|
||||||
|
// Don't clear screen on rebuild
|
||||||
|
clearScreen: false,
|
||||||
|
|
||||||
|
// Rebuild delay
|
||||||
|
buildDelay: 0,
|
||||||
|
|
||||||
|
// Watch chokidar options
|
||||||
|
chokidar: {
|
||||||
|
usePolling: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Watch Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch mode
|
||||||
|
rollup -c -w
|
||||||
|
|
||||||
|
# With environment variable
|
||||||
|
ROLLUP_WATCH=true rollup -c
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Development
|
||||||
|
|
||||||
|
### Plugin Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function myPlugin(options = {}) {
|
||||||
|
return {
|
||||||
|
// Plugin name (required)
|
||||||
|
name: 'my-plugin',
|
||||||
|
|
||||||
|
// Build hooks
|
||||||
|
options(inputOptions) {
|
||||||
|
// Modify input options
|
||||||
|
return inputOptions;
|
||||||
|
},
|
||||||
|
|
||||||
|
buildStart(inputOptions) {
|
||||||
|
// Called on build start
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveId(source, importer, options) {
|
||||||
|
// Custom module resolution
|
||||||
|
if (source === 'virtual-module') {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return null; // Defer to other plugins
|
||||||
|
},
|
||||||
|
|
||||||
|
load(id) {
|
||||||
|
// Load module content
|
||||||
|
if (id === 'virtual-module') {
|
||||||
|
return 'export default "Hello"';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
transform(code, id) {
|
||||||
|
// Transform module code
|
||||||
|
if (id.endsWith('.txt')) {
|
||||||
|
return {
|
||||||
|
code: `export default ${JSON.stringify(code)}`,
|
||||||
|
map: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buildEnd(error) {
|
||||||
|
// Called when build ends
|
||||||
|
if (error) {
|
||||||
|
console.error('Build failed:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Output generation hooks
|
||||||
|
renderStart(outputOptions, inputOptions) {
|
||||||
|
// Called before output generation
|
||||||
|
},
|
||||||
|
|
||||||
|
banner() {
|
||||||
|
return '/* Custom banner */';
|
||||||
|
},
|
||||||
|
|
||||||
|
footer() {
|
||||||
|
return '/* Custom footer */';
|
||||||
|
},
|
||||||
|
|
||||||
|
renderChunk(code, chunk, options) {
|
||||||
|
// Transform output chunk
|
||||||
|
return code;
|
||||||
|
},
|
||||||
|
|
||||||
|
generateBundle(options, bundle) {
|
||||||
|
// Modify output bundle
|
||||||
|
for (const fileName in bundle) {
|
||||||
|
const chunk = bundle[fileName];
|
||||||
|
if (chunk.type === 'chunk') {
|
||||||
|
// Modify chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
writeBundle(options, bundle) {
|
||||||
|
// After bundle is written
|
||||||
|
},
|
||||||
|
|
||||||
|
closeBundle() {
|
||||||
|
// Called when bundle is closed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default myPlugin;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin with Rollup Utils
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createFilter } from '@rollup/pluginutils';
|
||||||
|
|
||||||
|
function myTransformPlugin(options = {}) {
|
||||||
|
const filter = createFilter(options.include, options.exclude);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'my-transform',
|
||||||
|
|
||||||
|
transform(code, id) {
|
||||||
|
if (!filter(id)) return null;
|
||||||
|
|
||||||
|
// Transform code
|
||||||
|
const transformed = code.replace(/foo/g, 'bar');
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: transformed,
|
||||||
|
map: null // Or generate sourcemap
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Svelte Integration
|
||||||
|
|
||||||
|
### Complete Svelte Setup
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// rollup.config.js
|
||||||
|
import svelte from 'rollup-plugin-svelte';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import terser from '@rollup/plugin-terser';
|
||||||
|
import css from 'rollup-plugin-css-only';
|
||||||
|
import livereload from 'rollup-plugin-livereload';
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
function serve() {
|
||||||
|
let server;
|
||||||
|
|
||||||
|
function toExit() {
|
||||||
|
if (server) server.kill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
writeBundle() {
|
||||||
|
if (server) return;
|
||||||
|
server = require('child_process').spawn(
|
||||||
|
'npm',
|
||||||
|
['run', 'start', '--', '--dev'],
|
||||||
|
{
|
||||||
|
stdio: ['ignore', 'inherit', 'inherit'],
|
||||||
|
shell: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
process.on('SIGTERM', toExit);
|
||||||
|
process.on('exit', toExit);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/main.js',
|
||||||
|
output: {
|
||||||
|
sourcemap: true,
|
||||||
|
format: 'iife',
|
||||||
|
name: 'app',
|
||||||
|
file: 'public/build/bundle.js'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
compilerOptions: {
|
||||||
|
dev: !production
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
css({ output: 'bundle.css' }),
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
dedupe: ['svelte']
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
|
||||||
|
// Dev server
|
||||||
|
!production && serve(),
|
||||||
|
!production && livereload('public'),
|
||||||
|
|
||||||
|
// Minify in production
|
||||||
|
production && terser()
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Bundle Optimization
|
||||||
|
|
||||||
|
1. **Enable tree shaking** - Use ES modules
|
||||||
|
2. **Mark side effects** - Set `sideEffects` in package.json
|
||||||
|
3. **Use terser** - Minify production builds
|
||||||
|
4. **Analyze bundles** - Use rollup-plugin-visualizer
|
||||||
|
5. **Code split** - Lazy load routes and features
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
// Don't bundle peer dependencies for libraries
|
||||||
|
external: [
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
/^lodash\//
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development vs Production
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [
|
||||||
|
replace({
|
||||||
|
preventAssignment: true,
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(
|
||||||
|
production ? 'production' : 'development'
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
production && terser()
|
||||||
|
].filter(Boolean)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
onwarn(warning, warn) {
|
||||||
|
// Ignore circular dependency warnings
|
||||||
|
if (warning.code === 'CIRCULAR_DEPENDENCY') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore unused external imports
|
||||||
|
if (warning.code === 'UNUSED_EXTERNAL_IMPORT') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat other warnings as errors
|
||||||
|
if (warning.code === 'UNRESOLVED_IMPORT') {
|
||||||
|
throw new Error(warning.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default warning handling
|
||||||
|
warn(warning);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Library Build
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import pkg from './package.json';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/index.js',
|
||||||
|
external: Object.keys(pkg.peerDependencies || {}),
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: pkg.main,
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: pkg.module,
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Build
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
input: 'src/main.js',
|
||||||
|
output: {
|
||||||
|
dir: 'dist',
|
||||||
|
format: 'esm',
|
||||||
|
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||||
|
entryFileNames: '[name]-[hash].js',
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// All dependencies bundled
|
||||||
|
resolve({ browser: true }),
|
||||||
|
commonjs(),
|
||||||
|
terser()
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Worker Build
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default [
|
||||||
|
// Main application
|
||||||
|
{
|
||||||
|
input: 'src/main.js',
|
||||||
|
output: {
|
||||||
|
file: 'dist/main.js',
|
||||||
|
format: 'esm'
|
||||||
|
},
|
||||||
|
plugins: [resolve(), commonjs()]
|
||||||
|
},
|
||||||
|
// Web worker (IIFE format)
|
||||||
|
{
|
||||||
|
input: 'src/worker.js',
|
||||||
|
output: {
|
||||||
|
file: 'dist/worker.js',
|
||||||
|
format: 'iife'
|
||||||
|
},
|
||||||
|
plugins: [resolve(), commonjs()]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Module not found:**
|
||||||
|
- Check @rollup/plugin-node-resolve is configured
|
||||||
|
- Verify package is installed
|
||||||
|
- Check `external` array
|
||||||
|
|
||||||
|
**CommonJS module issues:**
|
||||||
|
- Add @rollup/plugin-commonjs
|
||||||
|
- Check `namedExports` configuration
|
||||||
|
- Try `transformMixedEsModules: true`
|
||||||
|
|
||||||
|
**Circular dependencies:**
|
||||||
|
- Use `onwarn` to suppress or fix
|
||||||
|
- Refactor to break cycles
|
||||||
|
- Check import order
|
||||||
|
|
||||||
|
**Sourcemaps not working:**
|
||||||
|
- Set `sourcemap: true` in output
|
||||||
|
- Ensure plugins pass through maps
|
||||||
|
- Check browser devtools settings
|
||||||
|
|
||||||
|
**Large bundle size:**
|
||||||
|
- Use rollup-plugin-visualizer
|
||||||
|
- Check for duplicate dependencies
|
||||||
|
- Verify tree shaking is working
|
||||||
|
- Mark unused packages as external
|
||||||
|
|
||||||
|
## CLI Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic build
|
||||||
|
rollup -c
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
rollup -c -w
|
||||||
|
|
||||||
|
# Custom config
|
||||||
|
rollup -c rollup.custom.config.js
|
||||||
|
|
||||||
|
# Output format
|
||||||
|
rollup src/main.js --format esm --file dist/bundle.js
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
NODE_ENV=production rollup -c
|
||||||
|
|
||||||
|
# Silent mode
|
||||||
|
rollup -c --silent
|
||||||
|
|
||||||
|
# Generate bundle stats
|
||||||
|
rollup -c --perf
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **Rollup Documentation**: https://rollupjs.org
|
||||||
|
- **Plugin Directory**: https://github.com/rollup/plugins
|
||||||
|
- **Awesome Rollup**: https://github.com/rollup/awesome
|
||||||
|
- **GitHub**: https://github.com/rollup/rollup
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- **svelte** - Using Rollup with Svelte
|
||||||
|
- **typescript** - TypeScript compilation with Rollup
|
||||||
|
- **nostr-tools** - Bundling Nostr applications
|
||||||
1004
.claude/skills/svelte/SKILL.md
Normal file
1004
.claude/skills/svelte/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
1
.gitignore
vendored
1
.gitignore
vendored
@@ -79,6 +79,7 @@ cmd/benchmark/data
|
|||||||
!*.svelte
|
!*.svelte
|
||||||
!.github/**
|
!.github/**
|
||||||
!.github/workflows/**
|
!.github/workflows/**
|
||||||
|
!.claude/**
|
||||||
!app/web/dist/**
|
!app/web/dist/**
|
||||||
!app/web/dist/*.js
|
!app/web/dist/*.js
|
||||||
!app/web/dist/*.js.map
|
!app/web/dist/*.js.map
|
||||||
|
|||||||
Reference in New Issue
Block a user