npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@redclover/koru-sdk

v1.1.1

Published

Lightweight SDK for building Koru widgets with TypeScript support, smart caching, and lifecycle management

Readme

Koru Widget SDK

npm version Bundle Size

Lightweight JavaScript SDK (~2KB gzipped) for building widgets that integrate with the Koru platform. Handles authorization, caching, error handling, and lifecycle management out of the box.

Table of Contents

Features

  • 🪶 Lightweight - Only 2.1KB gzipped
  • 🚀 Zero Dependencies - No external runtime dependencies
  • 📦 Multiple Formats - CommonJS, ESM, and UMD builds
  • 🔒 Smart Caching - LocalStorage with configurable TTL
  • 🔄 Retry Logic - Automatic authorization retries
  • 📊 Analytics Ready - Built-in event tracking
  • 📱 Mobile Detection - Responsive widget helpers
  • 🎯 TypeScript - Full type definitions included
  • 👁️ Preview Mode - Live preview support for Koru platform

Installation

npm install @redclover/koru-sdk

Quick Start

import { KoruWidget } from '@redclover/koru-sdk';

class MyWidget extends KoruWidget {
  constructor() {
    super({ name: 'my-widget', version: '1.0.0' });
  }

  async onInit(config) {
    // Setup logic - runs after authorization
    console.log('Widget config:', config);
  }

  async onRender(config) {
    // Render your widget UI
    this.container = this.createElement('div', {
      className: 'my-widget',
      children: [
        this.createElement('h1', { children: ['Hello World!'] }),
        this.createElement('button', {
          children: ['Click Me'],
          onClick: () => this.track('button_clicked')
        })
      ]
    });
    document.body.appendChild(this.container);
  }

  async onDestroy() {
    // Cleanup when widget is removed
    this.container?.remove();
  }
}

// Start the widget
new MyWidget().start();

HTML Integration

Add the widget script tag with required data attributes:

<script 
  src="https://cdn.example.com/my-widget.js"
  data-website-id="your-website-id"
  data-app-id="your-app-id"
  data-app-manager-url="https://app-manager.example.com"
></script>

Preview Mode

The SDK automatically supports Koru's live preview feature. When window.__KORU_PREVIEW_CONFIG__ is present, the widget uses it directly instead of making API authorization calls.

How it works:

  1. Koru platform sets preview configuration on the window object
  2. SDK detects the preview config during initialization
  3. Widget uses preview config instead of fetching from API
  4. No code changes needed - it's automatic!

Example preview setup:

<script>
  // Koru platform injects this before loading your widget
  window.__KORU_PREVIEW_CONFIG__ = {
    title: "Preview Widget",
    apiUrl: "https://api.example.com",
    items: [
      { id: "1", name: "Preview Item 1" },
      { id: "2", name: "Preview Item 2" }
    ]
  };
</script>
<script src="your-widget.js" data-website-id="..." data-app-id="..." data-app-manager-url="..."></script>

Debug logging:

When preview mode is active and debug is enabled, you'll see:

[my-widget] Using Koru preview config

This allows Koru to provide instant visual feedback when configuring widgets, without requiring full authorization flow.

API Reference

Constructor Options

class MyWidget extends KoruWidget {
  constructor() {
    super({ 
      name: 'widget-name',        // Required: Widget identifier
      version: '1.0.0',            // Required: Widget version
      options: {
        cache: true,               // Enable caching (default: true)
        cacheDuration: 3600,       // Cache TTL in seconds (default: 3600)
        retryAttempts: 3,          // Auth retry attempts (default: 3)
        retryDelay: 1000,          // Retry delay in ms (default: 1000)
        analytics: false,          // Enable analytics (default: false)
        debug: false               // Enable debug logs (default: false)
      }
    });
  }
}

Lifecycle Hooks

| Hook | When Called | Purpose | |------|-------------|---------| | onInit(config) | After authorization | Initialize widget state | | onRender(config) | After init | Render widget UI | | onDestroy() | On widget stop | Cleanup resources | | onConfigUpdate(config) | On reload (optional) | Update without full re-render |

Example:

class MyWidget extends KoruWidget {
  async onInit(config) {
    // Setup event listeners, fetch data, etc.
    this.data = await this.fetchData(config.apiUrl);
  }

  async onRender(config) {
    // Create and append DOM elements
    this.container = this.createElement('div', {
      className: 'widget',
      children: this.renderContent(config)
    });
    document.body.appendChild(this.container);
  }

  async onConfigUpdate(config) {
    // Optional: Update existing UI without full re-render
    this.updateContent(config);
  }

  async onDestroy() {
    // Remove event listeners, clear timers, remove DOM
    this.container?.remove();
  }
}

Helper Methods

createElement(tag, props)

Create DOM elements with properties:

const button = this.createElement('button', {
  className: 'btn btn-primary',
  style: { padding: '10px', color: 'white' },
  onClick: (e) => console.log('clicked'),
  children: ['Click Me', icon]
});

isMobile()

Detect mobile devices:

async onRender(config) {
  const layout = this.isMobile() ? 'mobile' : 'desktop';
  this.container = this.createElement('div', {
    className: `widget-${layout}`
  });
}

track(eventName, eventData)

Track analytics events (requires analytics: true):

this.track('button_clicked', {
  button_id: 'cta',
  timestamp: Date.now()
});

log(message, ...args)

Debug logging (requires debug: true):

this.log('Rendering widget', { config });

Public Methods

const widget = new MyWidget();

await widget.start();   // Initialize and start widget
await widget.stop();    // Stop and cleanup widget
await widget.reload();  // Reload with fresh config (clears cache)

Protected Properties

this.config      // Current widget configuration
this.authData    // Full authorization response
this.container   // Main container element (set in onRender)

Advanced Examples

State Management

class StatefulWidget extends KoruWidget {
  private state = {
    count: 0,
    items: []
  };

  async onRender(config) {
    this.state.items = config.items || [];
    this.render();
  }

  private render() {
    if (this.container) this.container.remove();

    this.container = this.createElement('div', {
      children: [
        this.createElement('p', { 
          children: [`Count: ${this.state.count}`] 
        }),
        this.createElement('button', {
          children: ['Increment'],
          onClick: () => {
            this.state.count++;
            this.render();
          }
        })
      ]
    });
    document.body.appendChild(this.container);
  }
}

Complex UI

class ComplexWidget extends KoruWidget {
  async onRender(config) {
    const header = this.createElement('header', {
      className: 'widget-header',
      children: [
        this.createElement('h1', { children: [config.title] }),
        this.createElement('p', { children: [config.description] })
      ]
    });

    const list = this.createElement('ul', {
      className: 'widget-list',
      children: config.items.map(item => 
        this.createElement('li', {
          children: [item.name],
          onClick: () => this.handleItemClick(item)
        })
      )
    });

    this.container = this.createElement('div', {
      className: 'complex-widget',
      children: [header, list]
    });

    document.body.appendChild(this.container);
  }

  private handleItemClick(item) {
    this.track('item_clicked', { item_id: item.id });
  }
}

Error Handling

class SafeWidget extends KoruWidget {
  async onRender(config) {
    try {
      this.renderContent(config);
    } catch (error) {
      this.log('Render error:', error);
      this.renderErrorState();
    }
  }

  private renderErrorState() {
    this.container = this.createElement('div', {
      className: 'widget-error',
      children: ['Failed to load widget. Please try again.']
    });
    document.body.appendChild(this.container);
  }
}

Proper Cleanup

class CleanWidget extends KoruWidget {
  private timers: number[] = [];
  private listeners: Array<{
    element: HTMLElement;
    event: string;
    handler: EventListener;
  }> = [];

  async onRender(config) {
    const button = this.createElement('button', {
      children: ['Click']
    });

    const handler = () => console.log('clicked');
    button.addEventListener('click', handler);
    this.listeners.push({ element: button, event: 'click', handler });

    const timer = setInterval(() => this.update(), 1000);
    this.timers.push(timer);

    this.container = this.createElement('div', {
      children: [button]
    });
    document.body.appendChild(this.container);
  }

  async onDestroy() {
    // Clean up event listeners
    this.listeners.forEach(({ element, event, handler }) => {
      element.removeEventListener(event, handler);
    });
    
    // Clear timers
    this.timers.forEach(timer => clearInterval(timer));
    
    // Remove DOM
    this.container?.remove();
  }
}

TypeScript

Full TypeScript support with included type definitions:

import { 
  KoruWidget,
  WidgetConfig,
  AuthResponse,
  WidgetOptions,
  CreateElementProps
} from '@redclover/koru-sdk';

class TypedWidget extends KoruWidget {
  private myData: string[] = [];
  private container!: HTMLDivElement;

  async onInit(config: WidgetConfig) {
    // Type-safe config access
    this.myData = config.items as string[];
  }

  async onRender(config: WidgetConfig) {
    // Full type inference for createElement
    this.container = this.createElement('div', {
      className: 'typed-widget',
      style: { padding: '20px' },
      children: [
        this.createElement('h1', { 
          children: [config.title as string] 
        })
      ]
    });
    document.body.appendChild(this.container);
  }

  async onDestroy(): Promise<void> {
    this.container?.remove();
  }
}

Type Definitions

The SDK exports the following types for enhanced IDE support:

  • KoruWidget - Abstract base class for widgets
  • WidgetConfig - Configuration object from Koru
  • AuthResponse - Full authorization response with metadata
  • WidgetOptions - Constructor options for widget configuration
  • CreateElementProps<K> - Generic props for createElement helper

IDE Setup

VS Code

For optimal development experience in VS Code:

  1. Install recommended extensions:

    • ESLint
    • TypeScript and JavaScript Language Features (built-in)
    • IntelliCode
  2. Enable IntelliSense: The SDK includes full JSDoc comments and TypeScript definitions. IntelliSense will automatically show:

    • Method signatures and descriptions
    • Parameter types and documentation
    • Return types
    • Usage examples
  3. Type checking:

    // Add to your tsconfig.json
    {
      "compilerOptions": {
        "strict": true,
        "noImplicitAny": true,
        "strictNullChecks": true
      }
    }

WebStorm / IntelliJ IDEA

WebStorm provides excellent TypeScript support out of the box:

  1. TypeScript definitions are automatically recognized
  2. JSDoc comments appear in quick documentation (Ctrl+Q / Cmd+J)
  3. Parameter hints show inline while typing

Auto-completion Features

The SDK provides rich autocomplete for:

  • Lifecycle hooks - Shows required and optional hooks with documentation
  • Helper methods - createElement, isMobile, track, log with examples
  • Configuration options - All constructor options with defaults
  • Element creation - Type-safe HTML element creation with proper props

Development

Build

npm run build

Generates:

  • dist/index.js - CommonJS build
  • dist/index.mjs - ESM build
  • dist/widget-sdk.min.js - UMD build (2.1KB gzipped)
  • dist/index.d.ts - TypeScript definitions

Watch Mode

npm run dev

Local Testing

npm link
# In your widget project:
npm link @redclover/koru-sdk

Publishing

npm login
npm publish --access restricted

Bundle Size

  • UMD (minified): 5.4KB (2.1KB gzipped)
  • ESM: 11KB
  • CommonJS: 11KB

Browser Support

  • Chrome 51+
  • Firefox 54+
  • Safari 10+
  • Edge 15+

Requires ES2015+ support, fetch API, and localStorage.

Troubleshooting

Widget Not Loading

Symptoms: Widget doesn't appear on the page

Solutions:

  1. Verify all data attributes are present on the script tag:
    <script 
      src="your-widget.js"
      data-website-id="your-website-id"
      data-app-id="your-app-id"
      data-app-manager-url="https://app-manager.example.com"
    ></script>
  2. Check that data-app-manager-url is correct and accessible
  3. Enable debug mode to see detailed logs:
    super({ name: 'my-widget', version: '1.0.0', options: { debug: true } });
  4. Check browser console for errors
  5. Verify the script is loading (check Network tab)

Authorization Failing

Symptoms: Widget loads but doesn't render, console shows authorization errors

Solutions:

  1. Verify data-website-id and data-app-id are correct
  2. Ensure the widget is authorized in Koru dashboard
  3. Check network tab for failed API requests (look for 401/403 errors)
  4. Verify the Koru URL is accessible from the browser
  5. Try clearing cache and reloading:
    const widget = new MyWidget();
    await widget.reload();

Cache Issues

Symptoms: Widget shows old data after configuration changes

Solutions:

// Option 1: Reload widget (clears cache automatically)
const widget = new MyWidget();
await widget.reload();

// Option 2: Disable caching during development
super({ 
  name: 'my-widget', 
  version: '1.0.0',
  options: { cache: false }
});

// Option 3: Reduce cache duration
super({ 
  name: 'my-widget', 
  version: '1.0.0',
  options: { cacheDuration: 60 } // 1 minute
});

TypeScript Errors

Symptoms: Type errors in IDE or during build

Solutions:

  1. Ensure TypeScript version is 4.0 or higher:
    npm install -D typescript@latest
  2. Check that types are properly imported:
    import { KoruWidget, WidgetConfig } from '@redclover/koru-sdk';
  3. Enable strict mode in tsconfig.json for better type safety

CORS Errors

Symptoms: Network requests fail with CORS errors

Solutions:

  1. Verify Koru is configured to allow requests from your domain
  2. Check that the data-app-manager-url uses the correct protocol (https)
  3. Contact Koru administrator to whitelist your domain

Memory Leaks

Symptoms: Page becomes slow over time, high memory usage

Solutions:

  1. Ensure proper cleanup in onDestroy:
    async onDestroy() {
      // Remove event listeners
      this.button?.removeEventListener('click', this.handleClick);
         
      // Clear timers
      clearInterval(this.timer);
         
      // Remove DOM elements
      this.container?.remove();
    }
  2. Avoid creating circular references
  3. Use WeakMap/WeakSet for object references when appropriate

Best Practices

1. Always Clean Up Resources

class BestPracticeWidget extends KoruWidget {
  private timers: number[] = [];
  private listeners: Map<HTMLElement, { event: string; handler: EventListener }> = new Map();

  protected addListener(element: HTMLElement, event: string, handler: EventListener) {
    element.addEventListener(event, handler);
    this.listeners.set(element, { event, handler });
  }

  async onDestroy() {
    // Clean up all listeners
    this.listeners.forEach(({ event, handler }, element) => {
      element.removeEventListener(event, handler);
    });
    this.listeners.clear();

    // Clear all timers
    this.timers.forEach(timer => clearInterval(timer));
    this.timers = [];

    // Remove DOM
    this.container?.remove();
  }
}

2. Use Debug Mode During Development

const isDev = process.env.NODE_ENV === 'development';

super({
  name: 'my-widget',
  version: '1.0.0',
  options: {
    debug: isDev,
    cache: !isDev, // Disable cache in development
    analytics: !isDev // Disable analytics in development
  }
});

3. Type Your Configuration

interface MyWidgetConfig extends WidgetConfig {
  apiUrl: string;
  title: string;
  items: Array<{ id: string; name: string }>;
}

class MyWidget extends KoruWidget {
  async onInit(config: WidgetConfig) {
    const typedConfig = config as MyWidgetConfig;
    // Now you have type safety
    console.log(typedConfig.apiUrl);
  }
}

4. Handle Errors Gracefully

class RobustWidget extends KoruWidget {
  async onRender(config: WidgetConfig) {
    try {
      await this.renderContent(config);
    } catch (error) {
      this.log('Render error:', error);
      this.renderErrorState();
    }
  }

  private renderErrorState() {
    this.container = this.createElement('div', {
      className: 'widget-error',
      style: { padding: '20px', color: 'red' },
      children: ['Failed to load widget. Please refresh the page.']
    });
    document.body.appendChild(this.container);
  }
}

5. Optimize for Performance

// Use onConfigUpdate for partial updates instead of full re-render
async onConfigUpdate(config: WidgetConfig) {
  // Only update changed elements
  if (this.titleElement) {
    this.titleElement.textContent = config.title as string;
  }
  // Much faster than destroying and re-rendering
}

// Debounce frequent operations
private debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: number;
  return (...args: Parameters<T>) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait) as unknown as number;
  };
}

License

Copyright © Red Clover. All rights reserved.

This software is proprietary and confidential. Unauthorized copying, distribution, modification, or use of this software, via any medium, is strictly prohibited.

Contributing

This is a proprietary SDK. For bug reports or feature requests, please contact the Red Clover team.

Support

For support, please reach out to:


Built for Koru | Documentation | Issues