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

@malv/rendering

v1.2.0

Published

Build-time HTML component rendering engine for Malv

Readme

@malv/rendering

Build-time HTML component rendering engine for Vite. Create reusable HTML components with scoped CSS and slots - no runtime JavaScript required for rendering.

Installation

npm install @malv/rendering

Quick Start

1. Create a component

<!-- components/my-button.html -->
<button class="my-button">
    <slot></slot>
</button>

<style>
.my-button {
    background: blue;
    color: white;
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
}
</style>

2. Configure Vite

// vite.config.ts
import { Rendering } from '@malv/rendering'

export default {
    plugins: [
        Rendering({
            'my-button': './components/my-button.html',
        })
    ]
}

3. Use in HTML

<!-- index.html -->
<my-button>Click me</my-button>

At build time, this becomes:

<button class="my-button">Click me</button>

API

Rendering(components, options?)

The main Vite plugin.

import { Rendering } from '@malv/rendering'

Rendering(components: ComponentsMap, options?: RenderingOptions)

folder(path)

Register all .html files in a directory as components. File names become tag names.

import { Rendering, folder } from '@malv/rendering'

export default {
    plugins: [
        Rendering({
            ...folder('./components'),
            // components/my-button.html → <my-button>
            // components/my-card.html → <my-card>
        })
    ]
}

component(name, source)

Register a single component.

import { Rendering, component } from '@malv/rendering'

export default {
    plugins: [
        Rendering({
            ...component('my-button', './components/my-button.html'),
        })
    ]
}

components(map)

Register multiple components at once.

import { Rendering, components } from '@malv/rendering'

export default {
    plugins: [
        Rendering({
            ...components({
                'my-button': './components/my-button.html',
                'my-card': './components/my-card.html',
            }),
        })
    ]
}

Component Sources

Components can be defined in several ways:

{
    // Relative file path
    'my-button': './components/my-button.html',
    
    // Absolute file path
    'my-card': { type: 'absolute', path: '/path/to/my-card.html' },
    
    // Raw HTML content
    'my-divider': { type: 'raw', html: '<hr class="divider" />' },
    
    // Dynamic import
    'my-dynamic': () => import('./components/my-dynamic.html'),
}

Features

Slots

Use <slot> to insert content passed to your component.

<!-- components/card.html -->
<div class="card">
    <slot></slot>
</div>
<!-- Usage -->
<card>
    <p>This content goes in the slot</p>
</card>

Named Slots

Use #slotname to create named slots.

<!-- components/card.html -->
<div class="card">
    <div class="card-header">
        <slot name="header"></slot>
    </div>
    <div class="card-body">
        <slot></slot>
    </div>
</div>
<!-- Usage -->
<card>
    <h2 #header>Card Title</h2>
    <p>Card content goes here</p>
</card>

Attributes

Attributes are passed to the #default element (or root element if none specified).

<!-- components/button.html -->
<button #default class="btn">
    <slot></slot>
</button>
<!-- Usage -->
<button disabled data-action="submit">Submit</button>

<!-- Result -->
<button class="btn" disabled data-action="submit">Submit</button>

Scoped Styles

Each component's <style> block is extracted and bundled. In development, styles update via HMR without page refresh.

<!-- components/button.html -->
<button class="btn"><slot></slot></button>

<style>
.btn {
    background: blue;
    color: white;
}
</style>

Component Scripts

Add <script> blocks for component-specific JavaScript.

<!-- components/counter.html -->
<div class="counter">
    <button class="decrement">-</button>
    <span class="count">0</span>
    <button class="increment">+</button>
</div>

<script>
document.querySelectorAll('.counter').forEach(counter => {
    const count = counter.querySelector('.count')
    counter.querySelector('.increment').onclick = () => {
        count.textContent = parseInt(count.textContent) + 1
    }
    counter.querySelector('.decrement').onclick = () => {
        count.textContent = parseInt(count.textContent) - 1
    }
})
</script>

Plugins

Create plugins to dynamically generate components from external data sources.

import { Rendering, type RenderingPlugin } from '@malv/rendering'
import fs from 'fs/promises'

const dataPlugin: RenderingPlugin = {
    name: 'product-components',
    filePaths: ['./data/products.json'],  // Changes trigger full reload
    
    async components() {
        const products = JSON.parse(
            await fs.readFile('./data/products.json', 'utf-8')
        )
        
        return products.map(product => ({
            name: `product-${product.id}`,
            filePaths: [`./data/products/${product.id}.json`],  // Changes trigger component reload
            
            async resolve() {
                const data = JSON.parse(
                    await fs.readFile(`./data/products/${product.id}.json`, 'utf-8')
                )
                return `
                    <div class="product">
                        <h2>${data.name}</h2>
                        <p>${data.price}</p>
                    </div>
                `
            }
        }))
    }
}

export default {
    plugins: [
        Rendering({}, { plugins: [dataPlugin] })
    ]
}

Options

interface RenderingOptions {
    plugins?: RenderingPlugin[]
}

How It Works

  1. Initialization: Components are loaded and their CSS/JS is extracted
  2. Transformation: HTML files are processed, replacing component tags with their content
  3. Slot Resolution: Content is inserted into <slot> elements
  4. Attribute Mapping: Attributes are applied to designated elements
  5. CSS Bundling: Extracted styles are bundled into a single CSS file
  6. HMR (Dev): Changes trigger granular updates without full page reload

Output

In production builds:

dist/
├── index.html           # Processed HTML with components expanded
├── mesa-styles/
│   └── indexAb3Cd-mesa.css   # Bundled component CSS
└── assets/
    └── ...              # Other assets

License

MIT