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

drupal-canvas

v0.7.1

Published

Utilities and base components for building Drupal Canvas Code Components

Readme

Drupal Canvas Code Components Utils

Utilities and base components for building Drupal Canvas Code Components.

React context hooks/providers are exported from drupal-canvas/react, together with CanvasContextProviderProps and JsonApiClientProviderProps. Context data and client types remain at the root. Existing components (FormattedText, Image, Region, RegionsProvider), utilities and legacy subpaths are unchanged; authoring helpers remain on drupal-canvas/json-render-utils.

Utilities

cn

Helper for combining Tailwind CSS classes using clsx and tailwind-merge. Implementation borrowed from shadcn/ui.

import { cn } from 'drupal-canvas';

export default function Example() {
  return <ControlDots className="absolute top-4 left-4 stroke-white" />;
}

const ControlDots = ({ className }) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 31 9"
    fill="none"
    strokeWidth="2"
    className={cn('w-12', className)}
  >
    <ellipse cx="4.13" cy="4.97" rx="3.13" ry="2.97" />
    <ellipse cx="15.16" cy="4.97" rx="3.13" ry="2.97" />
    <ellipse cx="26.19" cy="4.97" rx="3.13" ry="2.97" />
  </svg>
);

usePageContext and useSiteContext

Read the current page and site data in a React Code Component. Both hooks work in Drupal-rendered Code Components, Canvas Workbench previews, and React-based headless frontends: the rendering integration establishes the provider, and the hooks read it synchronously. They return null, with one actionable console warning, when no provider is mounted or the integration supplied no such data.

import { usePageContext, useSiteContext } from 'drupal-canvas/react';

export default function PageHeader() {
  const page = usePageContext();
  const site = useSiteContext();

  if (!page || !site) return null;

  return (
    <header>
      <a href={site.branding.homeUrl}>{site.branding.siteName}</a>
      <h1>{page.pageTitle}</h1>
    </header>
  );
}
  • Page context (PageContext): pageTitle, breadcrumbs, and mainEntity (the primary Drupal entity, null on routes without one), including language and translation information. See Main entity metadata.
  • Site context (SiteContext): branding, the Drupal baseUrl, and themeAssets. Headless frontends receive empty theme asset URLs; that is valid site context, so shared components must handle empty asset URLs.

Call the hooks unconditionally at the top level of a function component or custom hook (React's rules of hooks) and handle null results.

To reach components outside a Canvas tree, such as a site header in a headless application, wrap them in CanvasContextProvider:

import { CanvasContextProvider } from 'drupal-canvas/react';

<CanvasContextProvider context={page.context}>
  <SiteHeader />
</CanvasContextProvider>;

The CanvasContext type describes the provider's value: { page: PageContext | null; site: SiteContext | null }.

getPageData and getSiteData (deprecated)

getPageData() and getSiteData() are deprecated in favor of usePageContext() and useSiteContext(). They keep working in Drupal-rendered Code Components and Canvas Workbench previews, where they read drupalSettings, and they still report their data to the code editor's "Component data" panel. Outside those environments they throw an error that names the replacement API. Components used in both frontend modes must migrate to the hooks; canvas pull migrates safe calls automatically (see the Canvas CLI).

import { getPageData, getSiteData } from 'drupal-canvas';

const { pageTitle, breadcrumbs, mainEntity } = getPageData();
const { baseUrl, branding } = getSiteData();

sortLinksetMenu

Sort a menu linkset returned by Drupal core's linkset endpoint:

import { sortLinksetMenu } from 'drupal-canvas';

const { data } = useSWR('/system/menu/main/linkset', async (url) => {
  const response = await fetch(url);
  return response.json();
});
const menu = sortLinksetMenu(data);

getNodePath

Given a node returned from JSON:API, return either the path alias or fall back to the /node/[nid] path.

import { getNodePath } from 'drupal-canvas';

const articles = data.map((article) => ({
  ...article,
  _path: getNodePath(article),
}));

sortMenu

Sort menu items from the JSON:API Menu Items module into a tree with additional _children and _hasSubmenu properties.

import { JsonApiClient, sortMenu } from 'drupal-canvas';

const client = new JsonApiClient();
const { data } = useSWR(['menu_items', 'main'], ([type, resourceId]) =>
  client.getResource(type, resourceId),
);
const menu = sortMenu(data);

useJsonApiClient

Read a configured JSON:API client from context. The Drupal island renderer, the headless React renderer, and both Workbench preview paths provide it; the hook never fetches data or creates a client on render. Use it with SWR or another fetching library:

import { useJsonApiClient } from 'drupal-canvas/react';
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
import useSWR from 'swr';

export default function List() {
  const client = useJsonApiClient();
  const { data, error } = useSWR(client ? 'articles' : null, () =>
    client.getCollection('node--article', {
      queryString: new DrupalJsonApiParams()
        .addInclude(['field_tags'])
        .getQueryString(),
    }),
  );

  if (error) return 'An error has occurred.';
  // Test for data, not `isLoading`: with prefetched SWR fallback data the
  // data is present while SWR still reports loading during revalidation.
  if (!data) return 'Loading...';
  return (
    <ul>
      {data.map((article) => (
        <li key={article.id}>{article.title}</li>
      ))}
    </ul>
  );
}

The hook returns null, with one console warning, when no provider is mounted. In Drupal previews the client reads working copies (the rel:working-copy resource version) through the editor's session; in headless browsers it reaches Drupal through the application's same-origin proxy, authenticated from the draft preview session. Replacing the provided client does not clear SWR caches.

For portable components, use this hook rather than Drupal globals or a client with hardcoded URLs or credentials. Drupal, Workbench, and headless React integrations supply the client; the hook does not detect the environment. See Writing portable components for an SWR example and server-rendering guidance.

Drupal core's JSON:API module must be enabled.

Provide a client explicitly for components outside a Canvas tree with JsonApiClientProvider:

import { JsonApiClientProvider } from 'drupal-canvas/react';

<JsonApiClientProvider client={client}>
  <SiteHeader />
</JsonApiClientProvider>;

createJsonApiClient (drupal-canvas/jsonapi-client)

The shared, framework-agnostic client implementation rendering integrations and the Canvas Headless SDK build on. It extends @drupal-api-client/json-api-client with DefaultSerializer, draft reads at a configured resource version (collection items are hydrated with their working copies before serialization; raw responses bypass this), mapping of browser requests to a same-origin proxy (including absolute pagination links and the Decoupled Router endpoint), DraftSessionError for rejected preview sessions (the proxy's session error, or a 401 answered to a direct request that carried the session credentials), and caches separated by resource version and session scope: a client that may carry a session (authentication, cookies — explicit or a browser's same-origin default — the proxy, a custom fetch transport, a preview or a resource version) caches only with an explicit cacheScope, so the legacy client and any transport-injected client share no cache without a caller-provided scope. A 401 is a rejected session when the request carried the configured credentials (the Authorization header, cookies, or a transport declared with fetchAuthenticates); disableAuthentication opts a request out, and failing to obtain or renew credentials is a rejected session too. Draft collection reads hydrate each item with its working copy, keeping the read's locale, sparse fieldsets, includes, and an explicitly selected resource version; the working copies' included resources are merged into the document, and a resource that is itself primary is never duplicated in included, so relationships resolve to the selected working copy. URLs keep the backend's site path (https://host/sub/fr/jsonapi/..., also for the Decoupled Router and index lookups); apiUrl under baseUrl is a prefix override, and apiUrl on another site is a foreign JSON:API base: with apiSiteUrl (that site's base URL, install path included) a locale prefix goes between them (https://api.example/mount/fr/api), without it no locale prefix applies; the Decoupled Router stays under baseUrl either way.

import { createJsonApiClient } from 'drupal-canvas/jsonapi-client';

const client = createJsonApiClient({
  baseUrl: 'https://drupal.example',
  apiPrefix: 'jsonapi',
  // Browser clients in headless apps go through the app's proxy.
  proxyUrl: '/api/canvas/jsonapi',
  resourceVersion: 'rel:working-copy',
  preview: true,
});

DefaultSerializer and createCache are re-exported from drupal-canvas/jsonapi-client unchanged.

JsonApiClient (deprecated)

new JsonApiClient() is deprecated in favor of useJsonApiClient() in React Code Components and the Headless SDK's getClient() in headless server code. It keeps working in Drupal-rendered Code Components and Canvas Workbench previews, where it is configured from drupalSettings, and throws an error naming the replacement APIs elsewhere, even when a base URL is supplied.

import { JsonApiClient } from 'drupal-canvas';

const client = new JsonApiClient();

You can override the baseUrl and any default options:

const client = new JsonApiClient('https://drupal-api-demo.party', {
  serializer: undefined,
  cache: undefined,
});

Migrating from the deprecated APIs

Replace getPageData(), getSiteData(), and new JsonApiClient() with usePageContext(), useSiteContext(), and useJsonApiClient() in function components or custom hooks. Call hooks unconditionally at the top level before any possible return; handle missing context or clients. Outside components and custom hooks, use the Headless SDK's page context data or getClient() in headless server code; pass data or a client to browser helpers. Preserve output, types, hook order, and access controls. Never expose credentials.

json-render Utils

Utilities for working with json-render specs and Drupal Canvas component trees.

Note: These utilities currently depend on named slots support proposed for json-render in https://github.com/vercel-labs/json-render/pull/105.

canvasTreeToSpec

Converts a flat Drupal Canvas component tree to a json-render spec. Canvas stores components as a flat array linked by parent_uuid; json-render uses a spec object with a single root element and a flat map of elements linked by children and slots. This function builds the spec and, when there are multiple root components, wraps them in a synthetic canvas:component-tree element. Throws an error if the tree contains no root component.

import { canvasTreeToSpec } from 'drupal-canvas/json-render-utils';

const components = [
  {
    uuid: '872cde09-809a-4f48-8bf5-88f37127cb55',
    parent_uuid: null,
    slot: null,
    component_id: 'js.card',
    component_version: 'a681ae184a8f6b7f',
    inputs: { title: 'Hello' },
    label: 'Card',
  },
  {
    uuid: '87106237-b8d8-4e19-82f7-c780ad24feb5',
    parent_uuid: '872cde09-809a-4f48-8bf5-88f37127cb55',
    slot: 'body',
    component_id: 'js.text',
    component_version: 'd34b93534777207a',
    inputs: { content: 'World' },
    label: 'Text',
  },
];

const jsonRenderSpec = canvasTreeToSpec(components);

specToCanvasTree

Converts a json-render spec back to a flat Drupal Canvas component tree. Strips the synthetic canvas:component-tree wrapper if present, so multi-root trees round-trip cleanly.

import { specToCanvasTree } from 'drupal-canvas/json-render-utils';

const jsonRenderSpec = {
  root: 'card',
  elements: {
    card: {
      type: 'js.card',
      props: { title: 'Hello' },
      slots: { body: ['text'] },
    },
    text: {
      type: 'js.text',
      props: { content: 'World' },
    },
  },
};

const canvasComponentTree = specToCanvasTree(jsonRenderSpec);

renderSpec

Renders a json-render spec generated from canvas component tree using canvasTreeToSpec. The synthetic canvas:component-tree wrapper used for multi-root trees is handled internally and renders transparently. Unknown component types render nothing.

import { renderSpec } from 'drupal-canvas/json-render-utils';

import registry from './registry';

const spec = {
  root: 'card',
  elements: {
    card: {
      type: 'js.card',
      props: { title: 'Hello' },
      children: ['text'],
    },
    text: {
      type: 'js.text',
      props: { content: 'World' },
    },
  },
};

const rendered = renderSpec(spec, registry);

renderCanvasTree

Renders a Canvas component tree. Requires a ComponentRegistry for mapping component IDs to React components. Converts the tree to a json-render spec internally using canvasTreeToSpec and delegates to renderSpec. Unknown component types render nothing.

import { JsonApiClient } from 'drupal-canvas';
import { renderCanvasTree } from 'drupal-canvas/json-render-utils';
import useSWR from 'swr';

import registry from './registry';

const client = new JsonApiClient();

export function CanvasPage({ id }) {
  const { data: page } = useSWR(
    ['canvas_page--canvas_page', id],
    ([type, id]) => client.getResource(type, id),
  );

  if (!page) return null;

  return renderCanvasTree(page.components, registry);
}

defineComponentRegistry

Defines a component registry by dynamically importing each component's JavaScript entry file. Accepts an array of objects with name and jsEntryPath — compatible with DiscoveryResult.components from @drupal-canvas/discovery. Each module's default export is expected to be a render function. Components without a JS entry or without a default function export are skipped.

import { defineComponentRegistry } from 'drupal-canvas/json-render-utils';
import { discoverCanvasProject } from '@drupal-canvas/discovery';

const discovery = await discoverCanvasProject({ componentRoot: './src' });
const registry = await defineComponentRegistry(discovery.components);

defineComponentCatalog

Defines a complete json-render catalog from component metadata. Converts props from JSON Schema (as defined in component.yml) to Zod schemas. The returned catalog can be used with catalog.prompt() for AI prompt generation, catalog.validate() for spec validation, etc.

import { defineComponentCatalog } from 'drupal-canvas/json-render-utils';
import {
  discoverCanvasProject,
  loadComponentsMetadata,
} from '@drupal-canvas/discovery';

const discovery = await discoverCanvasProject({ componentRoot: './src' });
const metadata = await loadComponentsMetadata(discovery);
const catalog = defineComponentCatalog(metadata);
const systemPrompt = catalog.prompt();

Base Components

FormattedText

A built-in component to render text with trusted HTML using dangerouslySetInnerHTML. The content is safe when processed through Drupal's filter system that is correctly configured.

import { FormattedText } from 'drupal-canvas';

export default function Example() {
  return (
    <FormattedText>
      <em>Hello, world!</em>
    </FormattedText>
  );
}

Image

A built-in component for automatic image optimization, responsive behavior, and modern loading techniques for code components.

The Image component is a wrapper around the next-image-standalone library, preconfigured with a loader to work with the zero-config dynamic image style in Drupal Canvas.

import { Image } from 'drupal-canvas';

export default function MyComponent({ photo }) {
  return (
    <Image
      src={photo.src}
      alt={photo.alt}
      width={photo.width}
      height={photo.height}
    />
  );
}

Drupal generates no derivative images for an image its image toolkit cannot process, an SVG image for example. Such an image is rendered as-is, without a srcset. An SVG image that specifies neither its dimensions nor a viewBox is rendered without width and height too, and is then sized by the browser.

Region / RegionsProvider (deprecated)

Deprecated: theme-global regions were replaced by page variants, which compose a page from a single component tree. Both components keep working for compatibility, but no new region integration is added.

Render Drupal Canvas global regions inside a layout component. <Region name="..." /> slots in the region whose machine name matches name, and <RegionsProvider regions={...}> supplies the region node map. On the Drupal side, region placement is handled by the active theme; these components are used by standalone renderers (such as Workbench) that compose a page with its surrounding regions on their own.

import { Region } from 'drupal-canvas';

export default function Layout({ children }) {
  return (
    <>
      <Region name="header" />
      <main>{children}</main>
      <Region name="footer" />
    </>
  );
}

Pass a fallback to render placeholder content when a region is not provided:

<Region name="sidebar" fallback={<aside>No sidebar configured</aside>} />

When composing a renderer outside of Drupal or Workbench, wrap the tree in RegionsProvider with a map of region machine names to React nodes:

import { RegionsProvider } from 'drupal-canvas';

<RegionsProvider regions={{ header: <SiteHeader />, footer: <SiteFooter /> }}>
  <Layout>{pageContent}</Layout>
</RegionsProvider>;

Development

The following scripts are available for developing this package:

| Command | Description | | ------------ | ------------------------------------------------------------------------ | | build | Compile to the dist folder for production use. | | dev | Compile to the dist folder for development while watching for changes. | | type-check | Run TypeScript type checking without emitting files. | | test | Run tests. |