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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@genexus/kasstor-core

v0.3.0

Published

Decorators and directives to enhance Lit components for building web component libraries and applications.

Downloads

1,991

Readme

@genexus/kasstor-core

A set of decorators and directives to enhance Lit components with powerful features for building web component libraries and applications.

Table of Contents

Installation

npm i @genexus/kasstor-core

TypeScript configuration

Kasstor uses TypeScript decorators (the legacy/experimental kind), not the current ECMAScript standard decorators. Your tsconfig.json must enable them and use the right class field semantics so that @Component, @Event, @Observe, and @property work correctly.

Required:

  • experimentalDecorators: true — enables TypeScript decorators.
  • useDefineForClassFields: false — keeps the class field semantics that Lit and Kasstor decorators expect (property descriptors on the prototype). With true, TypeScript uses standard field semantics and decorators can break.

A working baseline (adjust paths and options to your project):

{
  "compilerOptions": {
    "target": "es2022",
    "experimentalDecorators": true,
    "useDefineForClassFields": false,
    "module": "es2022",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    // Bundler mode
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "outDir": "dist/",

    // Necessary to not add the src folder inside the dist
    "rootDir": "./src",

    // Linting
    "allowUnreachableCode": false,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noImplicitOverride": true,
    "noUncheckedSideEffectImports": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "strict": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "vite.config.ts", "**/*.e2e.ts"]
}

If you use a different build tool or emit TypeScript yourself, keep at least experimentalDecorators: true and useDefineForClassFields: false.

API

Decorators

Decorators are applied to classes and class members to define Kasstor custom elements, typed custom events, and reactive side effects. They are the main building blocks for components in this library.

Component

The @Component decorator is used to define a Kasstor custom element with support for advanced configuration.

Features

  • It automatically registers the custom element.

  • Shadow Root configuration (open/close mode, formAssociated, and delegatesFocus).

  • Better support for SSR in the components.

  • Support for styling components with SCSS/SASS.

  • Support for styling components without Shadow DOM.

  • Adds the firstWillUpdate life cycle method which works with SSR.

  • Adds support for the Observe decorator.

  • Support to define global styles outside of the component that work with and without Shadow DOM.

  • Support for HMR by using the @genexus/vite-plugin-kasstor package.

  • Improved initial rendering performance (compared by only extending the LitElement) by reducing the Total Blocking Time (TBT) in scenarios where many components are initially rendered.

Restrictions

  • The decorated class must extend KasstorElement (not LitElement directly).
  • The tag must be a valid custom element name with a hyphen (e.g. my-button). If the tag is already defined by another class, the decorator will not redefine it and a console warning is emitted (except under HMR).

Example

// button.scss

:host {
  // Styles
}

button {
  // Styles
}
// button.lit.ts

import { Component, KasstorElement } from "@genexus/kasstor-core/decorators/component.js";
import { Event, type EventEmitter } from "@genexus/kasstor-core/decorators/event.js";
import { html, nothing } from "lit";
import { property } from "lit/decorators.js";
import styles from "./button.scss?inline";

/**
 * A button with caption, disabled state, and a click event.
 * @access public
 */
@Component({
  tag: "my-button",
  styles,
  shadow: { delegatesFocus: true }
})
export class MyButton extends KasstorElement {
  /** Specifies the caption to show. */
  @property() caption: string = "Click me";

  /** Whether the button is disabled. */
  @property({ type: Boolean, reflect: true }) disabled: boolean = false;

  /** Emitted when the button is clicked. */
  @Event() protected click!: EventEmitter<void>;

  #onClick = (event: MouseEvent): void => {
    event.stopPropagation();
    this.click.emit();
  };

  override render() {
    return html`<button
      ?disabled=${this.disabled}
      type="button"
      @click=${this.disabled ? nothing : this.#onClick}
    >
      ${this.caption}
    </button>`;
  }
}

Types

const Component: <
  LibraryPrefix extends `${string}-`,
  Metadata,
  T extends typeof KasstorElement<Metadata>
>(
  options: ComponentOptions<LibraryPrefix, Metadata>
) => (target: T) => T | void;

/**
 * Options for the Component decorator
 */
export type ComponentOptions<LibraryPrefix extends `${string}-`, Metadata> = {
  globalStyles?: string;

  metadata?: Metadata;

  tag: `${LibraryPrefix}${string}`;

  /**
   * Specifies a set of options to customize the shadow root of the custom
   * element.
   *
   * By default, it assumes that the component uses Shadow DOM, so you only
   * need to provide the options for customizing the shadow root behavior.
   * If not specified, the following set of options are assumed:
   *   - `delegatesFocus: false`
   *   - `formAssociated: false`
   *   - `mode: "open"`
   *
   * If you don't want to use Shadow DOM, you can set this property to `false`.
   * We don't recommend using shadow: false, because slots (composition in
   * general) only exists when using shadow DOM, as well as style a JavaScript
   * encapsulation.
   */
  shadow?: ComponentShadowRootOptions | false;

  styles?: string;
};

export type ComponentShadowRootOptions = {
  /**
   * If `true`, when a non-focusable part of the shadow DOM is clicked, or
   * `.focus()` is called on the host element, the first focusable part inside
   * the host's shadow DOM is given focus, and the shadow host is given any
   * available `:focus` styling.
   *
   * If not specified, it uses `false` by default.
   */
  delegatesFocus?: boolean;

  /**
   * If `true`, it makes the [autonomous custom element](https://html.spec.whatwg.org/dev/custom-elements.html#autonomous-custom-element)
   * a [form-associated custom element](https://html.spec.whatwg.org/dev/custom-elements.html#form-associated-custom-element),
   * which is necessary when implementing custom elements that uses
   * [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
   * to work on forms.
   *
   * If not specified, it uses `false` by default.
   */
  formAssociated?: boolean;

  /**
   * This defines whether or not the shadow root's internal features are
   * accessible from JavaScript.
   *
   * If not specified, it uses `"open"` by default.
   */
  mode?: ShadowRootMode;
};

Event

The @Event decorator provides an easier way to define and dispatch custom DOM events with type safety and proper event configuration.

Features

  • Type-safe event emitting with generic types
  • Support for custom event details
  • Configurable event options (bubbles, cancelable, composed)
  • Automatic event emitter instance creation
  • Returns event object with defaultPrevented info

Restrictions

  • @Event() can only be applied to properties, not methods. Applying it to a method will throw in development mode.

API

  • Event(defaultOptions?: EventInit) — Optional default options (e.g. bubbles, cancelable, composed) for every emit. Can be overridden per call via emit(detail, options).
  • EventEmitter<T>.emit(detail?: T, options?: EventInit) — Dispatches a CustomEvent with detail and returns the event (e.g. to check defaultPrevented). Defaults: bubbles: true, cancelable: true.

Example

// input.lit.ts

import { Component, KasstorElement } from "@genexus/kasstor-core/decorators/component.js";
import { Event, type EventEmitter } from "@genexus/kasstor-core/decorators/event.js";
import { html } from "lit";
import { property } from "lit/decorators/property.js";

/**
 * Text input that emits the new value on change.
 * @access public
 */
@Component({ tag: "my-input" })
export class MyInput extends KasstorElement {
  /** Current value of the input. */
  @property() value: string = "";

  /** Emitted when the input value changes; detail is the new string. */
  @Event() protected input!: EventEmitter<string>;

  #onInput = (event: InputEvent): void => {
    event.stopPropagation();
    const oldValue = this.value;
    this.value = (event.target as HTMLInputElement).value;

    const eventInfo = this.input.emit(this.value);

    if (eventInfo.defaultPrevented) {
      event.preventDefault();
      this.value = oldValue;
    }
  };

  override render() {
    return html`
      <input
        type="text"
        placeholder="Insert value here..."
        .value=${this.value}
        @input=${this.#onInput}
      />
    `;
  }
}

Observe

The @Observe decorator executes a callback when observed properties change, e.g. @property or @state. This is useful for side effects and property validation.

Features

  • All callbacks for each Observe in a component are called in sync before the willUpdate lifecycle method of the component, and even before the firstWillUpdate. So, they are not called when a value for a property/state changes.

  • On the initial load, the Observe callback is called if the default value or the initial value of the component is not undefined. So, if you set undefined as the default value of the property/state or the host (that uses the component) sets undefined as initial value, the Observe callback won't be called.

  • Works with Server-Side Rendering (SSR).

  • Can observe single or multiple properties.

  • Changing a property/state inside of a Observe callback doesn't trigger an extra update.

  • Provides new and old values to callback.

Restrictions

  • @Observe must be applied to a method (the callback). Applying it to a non-function will throw in development mode.
  • The observed names must be properties that exist on the class and should be reactive (@property or @state). Observing a non-existent property will throw in development mode.

API

  • Observe(propertyOrProperties: string | string[]) — One property name or an array of property names. The decorated method is called with (newValue?, oldValue?) when any of them change.

Example

// observe-example.lit.ts

import {
  Component,
  KasstorElement,
  Observe
} from "@genexus/kasstor-core/decorators/component.js";
import { html } from "lit";
import { property, state } from "lit/decorators.js";

/**
 * Example component demonstrating @Observe on single and multiple properties.
 * @access public
 */
@Component({ tag: "my-observe-example" })
export class MyObserveExample extends KasstorElement {
  @state() private propBoolean: boolean = false;

  /** String property observed by propStringChanged. */
  @property() propString: string | undefined;

  /** Number property observed by propBooleanOrNumberChanged. */
  @property({ type: Number }) propNumber: number | undefined;

  @Observe("propString")
  protected propStringChanged(newValue?: unknown, oldValue?: unknown) {
    ...
  }

  @Observe(["propBoolean", "propNumber", ...])
  protected propBooleanOrNumberChanged(newValue?: unknown, oldValue?: unknown) {
   ...
  }
}

Bootstrapping

Bootstrapping APIs let library authors register custom element loaders so that components can be loaded on demand (e.g. when they appear in a template) instead of only via direct import.

registerCustomElementLoaders

Registers a set of custom element loaders for a library. Each loader is a function that returns a Promise (typically a dynamic import() of the component class). When a tag is used in the DOM (e.g. via the lazyLoad directive), the corresponding loader is run and the element is defined. This enables declarative lazy loading: components are loaded only when they are attached, improving initial load time.

Use case: You are building a component library (or an app) and want to support lazy loading. Call registerCustomElementLoaders once at startup with a map of tag names to CustomElementInfo (loader + optional dependencies); then any template can use <my-panel ${lazyLoad()}></my-panel> and the panel will be loaded when it is rendered.

How it works
  • Single global registry: The first call initializes a global registry (and a mutation observer). Later calls add or skip loaders based on prefix and tag.
  • Library prefix: All tag names in customElements must start with libraryPrefix (e.g. my-). The prefix identifies the library and avoids collisions; if the prefix is already registered by another library, the whole call is skipped and a warning is logged.
  • Per-tag registration: For each tag in customElements, if that tag is not yet registered, the loader and dependencies (or the default) are stored. If the tag was already registered by another library, it is skipped and a warning is logged.
  • Default for dependencies: If an entry omits dependencies, defaultCustomElementWatchingBehavior is used (either "never-observe" or "always-observe").
  • Already-attached elements: If elements with one of the new tags are already in the DOM (e.g. rendered before this call), their loaders are run so those elements get defined.
  • When lazyLoad runs: When an element with a registered tag is attached (e.g. Lit renders <my-panel ${lazyLoad()}></my-panel>), the runtime looks up the loader for that tag, runs it once, waits for the Promise, defines the custom element, and the component then renders. If the element was already defined (e.g. by a direct import), the loader is not run.
Options (LibraryLoaderOptions<Prefix>)
  • libraryName — Human-readable name for the library (used in warnings).
  • libraryPrefix — Prefix shared by all tags (e.g. "my-"). Type: LibraryPrefix (string ending with -).
  • defaultCustomElementWatchingBehavior — Default for entries that do not set dependencies: "never-observe" or "always-observe".
  • customElements — Record of tag name → CustomElementInfo (see below). Tag names must extend the prefix (e.g. "my-panel", "my-dashboard").
CustomElementInfo<TagName>

Each entry in customElements must conform to this type:

export type CustomElementExplicitDependencies<ElementItself = string> = Exclude<
  CustomElementTagNames | (string & {}),
  // Exclude the tag itself, as it is not a dependency
  ElementItself
>[];

export type CustomElementDependencies<ElementItself = string> =
  | CustomElementExplicitDependencies<ElementItself>
  | "never-observe"
  | "always-observe";

/**
 * Describes how to load a single custom element and how to observe its
 * Shadow DOM for nested custom elements. Used as the value type for each
 * entry in `LibraryLoaderOptions.customElements`.
 *
 * - **loader** — Required. Invoked when the element is first needed (e.g. by
 *   `lazyLoad`); must return a Promise that resolves when the component is
 *   defined. Typically `() => import("./my-panel.js")`. Cached per tag.
 * - **dependencies** — Optional. Controls Shadow DOM observation for nested
 *   tags: `"never-observe"` (default for many), `"always-observe"`, or an
 *   array of tag names. Omitted entries use the library's
 *   `defaultCustomElementWatchingBehavior`.
 */
export type CustomElementInfo<TagName extends string> = {
  /** Returns a Promise that resolves when the custom element is defined. */
  loader: () => Promise<unknown>;

  /**
   * Include the dependencies of your library's custom element tag.
   *
   * This property is important to **optimize** the render performance of
   * your library, by avoid observing mutations in custom elements that
   * don't use any other custom element, or where its dependencies are
   * already defined.
   *
   *  - `"never-observe"`: It means that the custom element doesn't have
   *    any dependencies, so it will not be observed for mutations in its
   *    Shadow Root.
   *
   *  - `"always-observe"`: The custom element will **always** be observed
   *    for mutations in its Shadow Root.
   *
   *  - When you specify an array of dependencies (tag names), it means
   *    that the custom element will be observed for mutations in its
   *    shadow root until all its dependencies are defined.
   *
   *    This is least common case, as most custom elements know their
   *    dependencies. This case can happen if you let customize the
   *    internal render of your custom element with a property that
   *    redefines the render. This is the case for the Chameleon's
   *    `ch-chat`, the internal render of messages can be customized with
   *    the `renderItem` function, so we can avoid observing that custom
   *    element.
   *
   *    **IMPORTANT!!**: Try to avoid this case, as it can lead to
   *    performance issues, because the custom element will always be
   *    observed for DOM mutations.
   */
  dependencies?: CustomElementDependencies<TagName>;
};

Example

import { registerCustomElementLoaders } from "@genexus/kasstor-core/bootstrapping/register-custom-element-loaders.js";

registerCustomElementLoaders({
  libraryName: "My Library",
  libraryPrefix: "my-",
  defaultCustomElementWatchingBehavior: "never-observe",
  customElements: {
    "my-panel": {
      loader: () => import("./components/my-panel.js")
    },
    "my-dashboard": {
      loader: () => import("./components/my-dashboard.js"),
      dependencies: "always-observe"
    }
  }
});

After this, templates can use <my-panel ${lazyLoad()}></my-panel> and the panel will be loaded when it is attached to the DOM.

Conflicts
  • If libraryPrefix is already in use by another library, the entire call is skipped and a warning is logged.
  • If a tag is already registered by another library, that tag is skipped and a warning is logged; other tags in the same call are still registered.

Directives

Directives are functions you use inside Lit templates to control how or when content is rendered. Kasstor provides a small set of directives that integrate with its component model and with SSR: they are designed to work with KasstorElement and to play well with server-side rendering and hydration. Each directive is imported from its own path under @genexus/kasstor-core/directives/.

lazyLoad

The lazyLoad directive automatically loads a component when it is attached to the DOM. It helps improve initial load time by loading components on demand.

Requirement: The component’s tag must be registered via registerCustomElementLoaders (see Bootstrapping). The library (or app) calls registerCustomElementLoaders at startup with loaders for each tag; lazyLoad then uses those loaders when the element is attached.

Features

  • Loads the component when the element is attached to the DOM; if the component is already registered, it is used immediately.
  • Can only be used in an ElementPart: it must be placed on the custom element tag (e.g. <my-component ${lazyLoad()}></my-component>).
  • Only works with components whose tags are registered via registerCustomElementLoaders (and that extend KasstorElement).

Example

import { Component, KasstorElement } from "@genexus/kasstor-core/decorators/component.js";
import { lazyLoad } from "@genexus/kasstor-core/directives/lazy-load.js";
import { html } from "lit";

/**
 * Root app component that lazy-loads the dashboard.
 * @access public
 */
@Component({ tag: "my-app" })
export class MyApp extends KasstorElement {
  override render() {
    return html`
      <my-header></my-header>
      <my-dashboard ${lazyLoad()}></my-dashboard>
      <my-footer></my-footer>
    `;
  }
}

Usage

  • Syntax: lazyLoad() with no arguments, on the element's attribute position.
  • Restriction: Using it in any other part type (e.g. ${lazyLoad()} inside node content) will throw at runtime, indicating it can only be used in an ElementPart.

renderByPlatform

The renderByPlatform directive renders content based on the current platform (browser or server).

Features

  • Server: renders serverValue; browser: renders browserValue. If serverValue is omitted, the server renders nothing for that expression (useful for browser-only content).
  • During hydration, keeps the server value until the first update, then switches to the browser value (avoids mismatch).
  • Only works when the template is rendered by a KasstorElement (needs host for hydration).

Example

import { Component, KasstorElement } from "@genexus/kasstor-core/decorators/component.js";
import { renderByPlatform } from "@genexus/kasstor-core/directives/render-by-platform.js";
import { html } from "lit";

/**
 * Renders different content on server vs browser using renderByPlatform.
 * @access public
 */
@Component({ tag: "my-render-by-platform-example" })
export class MyRenderByPlatformExample extends KasstorElement {
  override render() {
    return html`
      <h1>${renderByPlatform("Browser only", "Server only")}</h1>
      <p>${renderByPlatform("Client-only text")}</p>
    `;
  }
}

In the second line, only the browser will render "Client-only text"; the server renders nothing there. Use this for client-only UI or when the content depends on window or other browser APIs.

Restrictions

  • Must be used in a template rendered by a KasstorElement (the directive needs the host for hydration). Using it in a standalone template may break hydration.

Signature

  • renderByPlatform(browserValue, serverValue?): browserValue is shown on the client; serverValue on the server. If serverValue is omitted, the server renders nothing there (browser-only content).

Best Practices

Property Configuration

Use typed properties with explicit configuration for better type safety:

@property() title: string = '';

@property({ type: Number }) count: number = 0;

@property({ type: Boolean, reflect: true }) disabled: boolean = false;

Do's and Don'ts

Do

  • Extend KasstorElement (not LitElement) when using @Component; use a unique tag per component.

  • Use @Event() on a property typed as EventEmitter<Detail>; call this.myEvent.emit(detail) and check defaultPrevented when the event is cancelable.

  • Use @Observe on a method; pass only names of existing @property or @state fields. Prefer one responsibility per callback.

  • Use lazyLoad() only on the element tag (e.g. <my-panel ${lazyLoad()}></my-panel>). Ensure the component is registered in a Kasstor library.

  • Use renderByPlatform(browserValue, serverValue) when content must differ by environment (or when you want browser-only content; see Pro tip below).

Don't

  • Apply @Event() to a method or @Observe to a non-method.

  • Use shadow: false unless you have a strong reason (you lose slots and style encapsulation).

  • Use lazyLoad() in attribute or child positions; it only works on the element tag.

Pro tip: browser-only content with renderByPlatform

If you want to render something only in the browser (e.g. client-only UI, feature that depends on window), call renderByPlatform(browserValue) with a single argument. The server will render nothing for that expression, and after hydration the browser will show browserValue. If you want the same content on server and client, don't use the directive—render the value directly.

Contributing

Kasstor is open source and we appreciate issue reports and pull requests. See CONTRIBUTING.md for more information.