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

@papit/react

v0.0.2

Published

Generates typed React 19 wrapper components for a web component package from its source, so the elements can be used as React components with props, refs and on-event handlers.

Readme

@papit/react

Generates typed React 19 wrapper components for a web component package from its source, so the elements can be used as React components with props, refs and on-event handlers.

Logo


Type Tests NPM version


Installation

npm install @papit/react

Or run it without installing:

npx @papit/react

Usage

From inside a web component package, write src/react-wrapper.tsx:

npx @papit/react

As a package script:

{
  "scripts": {
    "generate:react": "npx @papit/react"
  }
}

Using the result (here for @papit/tabs, exported as @papit/tabs/react):

import { Tabs, Tab, TabPanel } from "@papit/tabs/react";

export function Example() {
    return (
        <Tabs value="a" onChange={(event) => console.log(event.target)}>
            <Tab value="a">First</Tab>
            <TabPanel value="a">First panel</TabPanel>
        </Tabs>
    );
}

Importing @papit/tabs/react also types the raw tags, e.g. <pap-tab value="a" />.

From code:

import { analysePackage } from "@papit/analyse";
import { generateReact, writeReact } from "@papit/react";

writeReact("packages/web/design-system/atoms/tabs", { output: "/tmp/tabs.tsx" });
const code = generateReact(analysePackage("packages/web/design-system/atoms/tabs"));

CLI flags

| Flag | Default | Description | | --- | --- | --- | | -o <file> | src/react-wrapper.tsx | Output file, relative to the package. |

API

writeReact

writeReact(location: string, options?: { output?: string }): {
  file: string; status: "written" | "skipped"; stale?: { file: string; removed: boolean };
}

Analyses the package in-process with @papit/analyse (no analyse.json needed) and writes the wrappers. It returns skipped when the file exists without the marker line (first, or second after "use client";).

With the default output, a src/react-wrapper.ts from before the .tsx is handled too, as bundle-js would still pick it first: deleted when it has the marker (stale.removed), kept with a warning when it was taken over by hand. A custom -o never touches it.

generateReact

generateReact(analysis: PackageAnalysis): string

The source of the wrapper file for an analysis, without touching disk.

MARKER

"// generated by @papit/react", the line after "use client"; in every generated file.

The generated file

Plain React, the way you'd write it by hand: a props interface and a function component per custom element, named after its class. It's .tsx, which @papit/bundle-js and @papit/bundle-ts pick up when there is no .ts of the same name.

  • SSR-safe. "use client"; comes first, and the package is only imported in the browser (if (typeof window !== "undefined") void import("@papit/tabs")), since defining elements needs the DOM. On the server the tags render as plain markup with their attributes. Every tag carries suppressHydrationWarning, as the element writes its own host attributes on connect (role, tabindex, reflected defaults).
  • Readable. One line per prop, one useEffect per event and one for the assigned props, no generic helpers. Each component returns ReactElement, so the emitted .d.ts stays short.
  • Intrinsic elements. The tags are declared in React.JSX.IntrinsicElements, so <pap-tabs> is typed too.

It isn't called react.tsx: every web package sets "baseUrl": "src", so a bare react import inside src/react.tsx would resolve to the file itself.

| Part | Comes from | | --- | --- | | Props | One name?: Element["name"] line per @property field over the whole extends chain, with its JSDoc description. Readonly and plain fields are left out. Names stay as the element spells them. An override without a decorator (override variant = "outline") keeps the options of the @property it overrides. A protected @property with an attribute is named after the attribute, dashes dropped (ismodal on modal → modal, close-outside-click → closeoutsideclick), and typed from its resolved type. | | How props reach the element | Strings, numbers, booleans and literal unions go through JSX, so they're server-rendered and React 19 sets them as properties once the element is defined. Aliases and indexed access types are judged by what @papit/analyse resolves them to (ButtonColor → "primary" \| … goes through JSX). One whose attribute name differs (hideStates → hide-states) goes through JSX under its attribute name. A boolean that defaults to true is rendered as "false" when false (loop={false} → loop="false"), since React would leave it out of the server markup and the element would boot with true. Objects, arrays, functions, unions with one of those (string \| Date), unresolved aliases and props without an attribute are assigned in a single effect per component after customElements.whenDefined, as an attribute would stringify or lose them. | | Events | Every event becomes an on<PascalCase> prop (change → onChange, tab-select → onTabSelect), except those inherited from @papit/web-component (e.g. first-render). An effect adds the listener on the element and removes it on unmount or when the handler changes. A CustomEvent is typed with its detail when the detail needs no import, otherwise as plain CustomEvent. | | ref | A normal prop (React 19), merged with the component's own ref. Object and callback refs both work, including callback cleanups. | | HTML attributes | HTMLAttributes<Element>, minus every key that clashes with a prop or handler (e.g. React's own onChange). | | Intrinsic element | The props interface, minus the handlers and the props the component doesn't pass through as they are, plus each renamed prop under its attribute name (<pap-confetti click>), typed from the props interface. A boolean that defaults to true also takes "false". |

The package is imported by its own name, for the element types and to define the elements. @papit/bundle-js keeps that import external in every entry but the main one, so customElements.define only runs once.

Taking it over

Delete the marker line and the file is yours: later runs print a message and leave it untouched.

Wiring a package

The package needs, besides the script:

  • exports["./react"], so the import path stays @papit/<pkg>/react:

    "./react": { "import": "./lib/react-wrapper.js", "types": "./lib/react-wrapper.d.ts" }
  • react (^19) as a peerDependency, so it and react/jsx-runtime stay out of the bundle

  • tsconfig "jsx": "react-jsx"

  • tsconfig paths mapping its own name to ./index, which the create template already sets up

License

Licensed under the @Papit License 1.0 Copyright (c) 2024 Henry Pap (@onkelhoy)

Key points

  • ✅ Free to use in commercial projects
  • ✅ Free to modify and distribute
  • ✅ Attribution required
  • ❌ Cannot resell the component itself as a standalone product

See the LICENSE file for full details.


Related