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

@hozo/metro

v0.3.0

Published

Metro integration for the Hozo compiler (Native lowering backend).

Readme

@hozo/metro

Metro integration for Hozo's React Native backend. Lowers View/Text/Pressable/Button from @hozo/core into real React Native primitives plus a StyleSheet.create({...}), rewriting each component in place.

The integration is exercised by development and minified production Metro bundles. Physical-device validation is still pending.

Setup

metro.config.js:

const { getDefaultConfig } = require('expo/metro-config') // or '@react-native/metro-config'
const { withHozo } = require('@hozo/metro/config')

const projectRoot = __dirname
const config = getDefaultConfig(projectRoot)
module.exports = withHozo(config, { root: projectRoot })

Metro accepts a promised config, so withHozo can resolve the Tailwind theme and generate the dynamic candidate module before bundling begins. It preserves the supplied config and records an existing Babel transformer as its upstream, including Expo's or another tool's transformer.

Importing from @hozo/core costs only what you use

Metro does not tree-shake, so a plain import { Button } from '@hozo/core' would load every pattern, primitive and form control the facade re-exports, used or not. The transformer rewrites named imports from @hozo/core, @hozo/patterns, @hozo/primitives, @hozo/semantics, @hozo/typography, @hozo/behaviors and @hozo/form to the modules that define each name, so an app pays for the components it imports. On the example app this took Hozo's share of the bundle from 591 KB to 436 KB.

This happens only for files Metro transforms through withHozo. A project that resolves Hozo packages through a plain Metro config still gets the whole facade. Web bundlers (Vite, webpack, Rollup) tree-shake the same imports themselves. A type import, a namespace import (import * as Hozo) or a name the rewrite cannot trace stays on the barrel.

TypeScript

Nothing to configure, as long as the project's tsconfig.json extends expo/tsconfig.base or @react-native/typescript-config. Both set:

{ "compilerOptions": { "moduleResolution": "bundler", "customConditions": ["react-native"] } }

which is what makes tsc resolve the same entry point Metro does. Every Hozo package publishes two — index.js and index.native.js — behind a react-native export condition, and TypeScript matches that condition only when it is named. A hand-written tsconfig.json that extends neither preset has to say it itself; customConditions requires moduleResolution to be bundler, node16 or nodenext.

Without it, an app is type-checked against the Web declarations: a native-only export such as HozoText from @hozo/core/generated/text reads as missing, and a component whose two halves differ is checked against the wrong half. It still bundles and runs correctly — this is the type layer only — which is why it goes unnoticed.

What the generated candidate module is for

A className Hozo can't read statically — <View className={getVariant()} /> — still has to produce styles. On Web that's free: the class string reaches the DOM and the browser matches it against a generated stylesheet. React Native has no CSS engine, so the string has to be resolved on device.

withHozo scans the project for class-shaped strings and writes node_modules/.hozo/candidates.native.js: a class-name → style-object map plus a resolver bound to it (from @hozo/core/generated/class-resolver, so an app that installed only @hozo/core resolves it under strict pnpm). Files with an unreadable className import it; files without one don't. The same module carries the resolved preflight choice to Native fallback components, so an uncompiled Heading, Separator, Small, Sub, or Sup uses the same browser defaults as compiled code. Metro redirects @hozo/engine/project automatically; no provider or application import is required. The lower-level generateCandidateModule API remains available from @hozo/metro/project.

It runs at config load rather than inside the transformer because Metro transforms in jest-worker subprocesses. Scanning there would mean several processes writing one cache file; the config layer is ordinary main-process code, so there's exactly one writer.

Limitation: the module is generated once, at config load. A class that only becomes a candidate after Metro started — a new string literal in a helper module — needs a Metro restart to appear. (react-native-css documents the same restriction for its transformer.)

Limitation: only unconditional utilities survive this path (the runtime-resolved one). A style object can't express hover:, md:, or pressed:, and making it able to would mean per-component state tracking — a runtime CSS engine, which Hozo deliberately doesn't ship. Those classes are recorded with the reason and warned about when they're actually used, not at build time: appearing in the scan doesn't prove any expression ever produces one. Write them as a static className and they compile to a real style variant with no runtime involved.

dark: and breakpoints need a component function

Written as a static className, dark: and sm:/md:/lg:/xl:/2xl: compile to a React hook from @hozo/engine, spliced as a statement at the top of the enclosing component:

export function Card() {
  const __hozoDark = useHozoDark()
  return <View style={[styles.hozo_r0_0, __hozoDark && styles.hozo_r0_0_dark]} />
}

The hook has to be a statement. Inlining the call into the JSX (style={[a, useHozoDark() && b]}) breaks the rules of hooks as soon as the element sits behind a conditional, so JSX at module scope or in a concise arrow body (() => <View className="dark:..." />) is a build error naming the fix.

@hozo/engine keeps one subscription per app, not one per component, and its snapshot is a coarse value — the breakpoint's name rather than the raw width. A resize that doesn't cross a breakpoint therefore re-renders nothing, and Android's keyboard-driven dimension events never reach it at all, since only width is an input.

Errors vs. warnings

Error-severity diagnostics stop the build. The case that exists for is a Web-only utility (block, grid, h-screen) reaching the Native backend: there's no correct output, so continuing would ship a layout that looks right on Web and is silently wrong on device.

Everything else is a warning printed during the build.

Existing React Native and Expo projects

Hozo compiles a file whose primitives come from react-native, with no migration to @hozo/core:

import { View, Text } from 'react-native'

export function Card() {
  return (
    <View className="rounded-xl p-4">
      <Text className="font-bold">Hello</Text>
    </View>
  )
}

The compiler always handled this — it matches on the JSX tag name and never asks where the name came from, which is what proposal §2.1 promises. What did not was the gate in front of it: every integration tested code.includes('@hozo/core'), so an Expo project was skipped for not having been rewritten.

Matching on the tag name is also why the gate cannot simply be widened. A <View> from a third-party component library has its own props and its own layout, and lowering it to a React Native View because the tag is spelled View would replace someone's component with a different one. So a file is compiled when every primitive-named binding in it comes from a module the project trusts:

withHozo(config, { sources: ['@hozo/core', 'react-native', './src/ui'] })

The default is ['@hozo/core', 'react-native']. A file importing only from modules outside that list is left alone silently — a project whose own components are named View is not doing anything wrong. A file mixing the two is left alone with a warning, because there its author has every reason to expect lowering.

@expo/ui and other component libraries

@expo/ui exports Text, Button, List, ListItem, ScrollView and TextInput — every one a native SwiftUI or Jetpack Compose control that shares nothing with the Hozo primitive of the same name but its spelling. A compiler matching on tag names alone cannot tell them apart.

So the module list travels into the compiler and resolution happens per tag. This file compiles:

import { View } from 'react-native'
import { Button, Host } from '@expo/ui/swift-ui'

export function Screen() {
  return (
    <View className="p-4">
      <Host><Button label="Save" /></Host>
    </View>
  )
}

The View lowers and gets its styles; the <Host><Button/></Host> is carried verbatim, the same treatment any component Hozo does not model already gets. Refusing the whole file would have left the half Hozo understands uncompiled; accepting it would have turned a SwiftUI button into a Pressable.

Two Native-side guards had to learn the same distinction: a carried <Button> is normally a build error, because Hozo's Button is a semantic primitive and React Native's takes a title and renders no children — but a carried @expo/ui Button is the correct outcome. And the generated react-native import skips any name the file already binds to something else.


Part of Hozo, a Rust-powered universal UI compiler and accessibility-first layer for React Native. Most applications install @hozo/core and one build integration; see Getting started. Source and issues: github.com/iray-tno/hozo.