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

@crystallize/plugin-signal

v0.6.0

Published

Typed JSON-RPC 2.0 messaging between a Crystallize plugin iframe and the parent app, with zod-validated method contracts.

Downloads

949

Readme

@crystallize/plugin-signal

Typed JSON-RPC 2.0 messaging between a Crystallize plugin iframe and the parent app, with zod-validated method contracts.

Install

pnpm add @crystallize/plugin-signal

Quick start

import { createChannel, createWindowTransport, type DataGridPayload } from '@crystallize/plugin-signal';

const channel = createChannel({
  transport: createWindowTransport(),
  handlers: {
    setDataGrid: (channel, payload) => renderGrid(payload),
  },
});

// Tell the parent we are mounted — it will respond with setDataGrid
channel.notify('ready', undefined);

// Send edits back
function onUserSavesEdits(payload: DataGridPayload) {
  channel.notify('updateDataGrid', payload);
}

function onUserClicksClose() {
  channel.notify('closePluginDialog', undefined);
  channel.dispose();
}

createWindowTransport() defaults to the plugin-iframe context:

| Option | Default | Meaning | |---|---|---| | target | globalThis.parent | The Crystallize app window | | targetOrigin | '*' | Passed to postMessage | | allowedOrigin | validParentOrigin | Only accept incoming messages from known Crystallize parent origins | | source | globalThis | The plugin iframe's own window |

Override any field if you need different behavior.

Methods

Every method has a zod schema for params and result in src/schemas.ts. Both sides validate on send and receive — invalid params produce a JSON-RPC -32602 Invalid params error.

| Method | Direction | Params | Result | |---|---|---|---| | ready | child → parent | — | — | | setDataGrid | parent → child | DataGridPayload | — | | setEntityContext | parent → child | EntityContext | — | | updateDataGrid | child → parent | DataGridPayload | — | | closePluginDialog | child → parent | — | — | | notifyHeight | child → parent | HeightPayload | — | | notifyWidth | child → parent | WidthPayload | — | | promoteToDialog | child → parent | PromoteToDialogPayload | — | | demoteFromDialog | child → parent | — | — | | updateItemComponent | child → parent | UpdateItemComponentParams | UpdateItemComponentResult | | refetchItem | child → parent | RefetchTargetPayload | — | | refetchItemComponents | child → parent | RefetchTargetPayload | — | | refetchItemVariantComponents | child → parent | RefetchTargetPayload | — |

notifyHeight lets the plugin request a target iframe height in integer pixels — the host can use it to resize the iframe to fit its content.

notifyWidth lets the plugin request a target iframe width in integer pixels — the host can use it to resize the iframe (e.g. a sidebar) to fit its content.

Dialog promotion

promoteToDialog lets a plugin ask the host to render its iframe as a centered floating dialog. demoteFromDialog returns it to its original slot. Both signals are fire-and-forget and direction child → parent.

// child side
channel.notify('promoteToDialog', { width: 800, height: 600 });
// later
channel.notify('demoteFromDialog', undefined);
// parent side
const channel = createChannel({
  transport,
  handlers: {
    promoteToDialog: (_, { width, height }) => host.promote({ width, height }),
    demoteFromDialog: () => host.demote(),
  },
});

PromoteToDialogPayload is { width?: number; height?: number }. Either dimension may be omitted — the host falls back to a sensible default and clamps oversized values to the viewport.

The same iframe element keeps running across promote/demote. Only the wrapper's CSS positioning and size change — no reload, no loss of in-iframe state.

Hosts that don't support promotion (e.g. plugins already running inside plugin-dialog) treat both signals as silent no-ops.

Updating an item component

updateItemComponent is a request/response signal: plugins write a value to any component path on an item or variant. The host injects the value into the matching rendered leaf component if the catalogue editor is open at that entity/language/sku.

const result = await channel.call('updateItemComponent', {
  itemId: 'abc123',
  language: 'en',
  componentId: 'summary-chunk/0/summary',
  input: { singleLine: { text: 'New title' } },
});

if (!result.applied) {
  // result.reason: 'NOT_RENDERED' | 'TYPE_MISMATCH'
  // result.message: string
}

Result (one of):

  • { applied: true } — value was injected into the matching rendered leaf.
  • { applied: false, reason: 'NOT_RENDERED', message } — no leaf currently rendered at the requested (itemId, language, sku ?? '', componentId).
  • { applied: false, reason: 'TYPE_MISMATCH', message } — a leaf is rendered at that path, but the input's discriminator does not match the leaf's component type.

The input shape is ComponentContentInput from @crystallize/schema/pim (with the inner componentId omitted) — every leaf shape (singleLine, richText, numeric, boolean, datetime, files, images, videos, location, selection, itemRelations, propertiesTable, paragraphCollection) plus every structural shape (contentChunk, componentChoice, componentMultipleChoice, piece).

Refetching catalogue data

refetchItem, refetchItemComponents, and refetchItemVariantComponents let a plugin ask the catalogue editor to re-fetch its data after the plugin has changed something server-side. All three are fire-and-forget notifications sharing the same payload:

// child side
channel.notify('refetchItem', { itemId: 'abc123', language: 'en' });
channel.notify('refetchItemComponents', { itemId: 'abc123', language: 'en' });
channel.notify('refetchItemVariantComponents', { itemId: 'abc123', language: 'en' });

RefetchTargetPayload is { itemId: string; language: string } (both required, non-empty).

  • refetchItem — re-fetches the base item (name, tree position, etc.).
  • refetchItemComponents — re-fetches the item-level components.
  • refetchItemVariantComponents — re-fetches the variant-level components of the variant currently displayed in the editor. The signal carries no sku; the editor refetches whichever variant it is showing for that itemId/language.

Silently ignored when not applicable. The host only acts if the catalogue editor is currently open on that itemId in that language. If the user is viewing a different item, a different language, or is not in the catalogue at all, the signal is dropped with no effect and no response. Any plugin iframe can fire these — not only one rendered in the catalogue.

Entity context

setEntityContext is fired by the host every time the user navigates within the Crystallize app after the iframe has rendered. The initial value is still part of the encrypted payload posted to the iframe at mount.

// child side
const channel = createChannel({
  transport: createWindowTransport(),
  handlers: {
    setEntityContext: (_channel, ctx) => {
      if (ctx.entity === 'catalogue' && ctx.itemId) {
        // ctx.itemType, ctx.variantSku, ctx.itemLanguage are available here
      }
    },
  },
});

EntityContext is a discriminated union on the entity field. Each variant has only the optional identifier and sub-state fields that apply to its section. The host always populates the base fields — pathname, searchParams, placement, tenantIdentifier, language — on every variant.

| Variant | Notable optional fields | |---|---| | dashboard | — | | catalogue | itemType, itemId, variantSku, subView, itemLanguage, itemsType, selectedRowIds | | topics | topicId, mode | | grids | gridId, mode | | orders | orderId, subscriptionId, mode | | customers | identifier, mode | | flows | identifier, flowType, mode | | insight | section | | fulfilment | pipelineId, mode | | subscription-contracts | contractId, mode | | assets | imageKey | | changelog | documentId | | special-prices | section | | marketplace | path | | apps | identifier | | settings | section | | unknown | — |

zEntityContext is exported for callers that want runtime validation outside of the protocol pipeline.

DataGridPayload is generic across every grid in the Crystallize app (explorer, customers, orders, price-lists, promotions, etc.) — its DataGridCellValue discriminated union covers every cell kind (text, number, boolean, uri, bubble, image-text, assets, videos, rich-text, number-unit, selection, topics, item-relation, meta-data). Adding new cell kinds is non-breaking for existing plugins.

Errors

channel.call() rejects with an RpcError whose code follows the JSON-RPC 2.0 spec, plus the package-specific extensions:

| Code | Meaning | |------|---------| | -32700 | Parse error | | -32600 | Invalid request | | -32601 | Method not found | | -32602 | Invalid params (failed zod validation on receive) | | -32603 | Internal error | | -32000 | Handler threw | | -32002 | Timeout | | -32003 | Channel disposed |

import { RpcError, RpcErrorCode } from '@crystallize/plugin-signal';

try {
  await channel.call('updateDataGrid', payload);
} catch (error) {
  if (error instanceof RpcError && error.code === RpcErrorCode.Timeout) {
    // ...
  }
}