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

hyper-morph

v0.2.0

Published

content-based DOM morphing library

Downloads

8

Readme

HyperMorph

Content-based DOM morphing. An enhanced Idiomorph that preserves element identity without explicit IDs.

The Problem

Positional matching fails on reorders and prepends:

<!-- Before -->                    <!-- After -->
<ul>                               <ul>
  <li>Apple</li>   ← position 0      <li>NEW</li>     ← position 0
  <li>Banana</li>  ← position 1      <li>Apple</li>   ← position 1
</ul>                                <li>Banana</li>  ← position 2
                                   </ul>

Positional morph: "Apple" DOM node gets text changed to "NEW". Focus lost, animations break, state resets.

HyperMorph: Recognizes "Apple" moved to position 1, preserves the DOM node.

How It Works

┌─────────────────────────────────────────────────────────────────┐
│                         MATCHING ALGORITHM                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   OLD TREE                          NEW TREE                     │
│                                                                  │
│   ┌──────────┐                      ┌──────────┐                │
│   │ sig: a3f │◄──── SIGNATURE ─────►│ sig: a3f │                │
│   │ path: #m │      LOOKUP          │ path: #m │                │
│   └──────────┘                      └──────────┘                │
│        │                                  │                      │
│        └──────────► SCORE PAIR ◄──────────┘                      │
│                    ┌──────────┐                                  │
│                    │ sig=+100 │                                  │
│                    │path=+30  │                                  │
│                    │text=+20  │                                  │
│                    │conf=150  │                                  │
│                    └────┬─────┘                                  │
│                         │                                        │
│                         ▼                                        │
│                 confidence ≥ 101?                                │
│                    YES → MATCH (move DOM node)                   │
│                    NO  → RECREATE                                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Signature = hash of tag + classes + key attributes Path = structural address relative to landmarks (IDs, roles, semantic tags) Threshold = 101 (signature alone isn't enough; requires additional signal)

Installation

npm install hyper-morph

Usage

import HyperMorph from 'hyper-morph';

// Basic morph
HyperMorph.morph(oldElement, newContent);

// With options
HyperMorph.morph(oldElement, newContent, {
  morphStyle: 'innerHTML',
  callbacks: {
    beforeNodeMorphed: (oldNode, newNode) => console.log('morphing', oldNode)
  }
});

Configuration Reference

Top-Level Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | morphStyle | 'outerHTML' \| 'innerHTML' | 'outerHTML' | Replace element itself or just its children | | ignoreActive | boolean | false | Skip morphing the focused element entirely | | ignoreActiveValue | boolean | false | Preserve value of focused input/textarea | | restoreFocus | boolean | true | Restore focus and selection after morph |

HyperMorph.morph(el, html, {
  morphStyle: 'innerHTML',
  ignoreActive: false,
  ignoreActiveValue: true,
  restoreFocus: true
});

Callbacks

Hook into the morph lifecycle. Return false from "before" callbacks to prevent the action.

| Callback | Signature | Description | |----------|-----------|-------------| | beforeNodeAdded | (node) => boolean | Before inserting new node. Return false to skip. | | afterNodeAdded | (node) => void | After node inserted | | beforeNodeMorphed | (oldNode, newNode) => boolean | Before morphing. Return false to skip. | | afterNodeMorphed | (oldNode, newNode) => void | After node morphed | | beforeNodeRemoved | (node) => boolean | Before removing. Return false to keep. | | afterNodeRemoved | (node) => void | After node removed | | beforeAttributeUpdated | (attr, el, type) => boolean | Before attribute change. type is 'update' or 'remove'. |

HyperMorph.morph(el, html, {
  callbacks: {
    beforeNodeAdded: (node) => {
      if (node.classList?.contains('skip')) return false;
    },
    afterNodeMorphed: (oldNode, newNode) => {
      console.log('Morphed:', oldNode);
    },
    beforeAttributeUpdated: (attr, el, type) => {
      if (attr === 'data-persist') return false; // prevent update
    }
  }
});

Head Configuration

Control how <head> elements are handled during full-document morphs.

| Option | Type | Default | Description | |--------|------|---------|-------------| | style | 'merge' \| 'append' \| 'morph' \| 'none' | 'merge' | Merge strategy for head elements | | block | boolean | false | Wait for new stylesheets/scripts to load before morphing body | | ignore | boolean | false | Skip head morphing entirely | | shouldPreserve | (el) => boolean | Check im-preserve | Keep element even if not in new content | | shouldReAppend | (el) => boolean | Check im-re-append | Remove and re-add (re-executes scripts) | | shouldRemove | (el) => boolean | () => {} | Return false to prevent removal | | afterHeadMorphed | (head, {added, kept, removed}) => void | noop | Called after head processing |

Head styles:

  • 'merge' — Add new elements, remove old ones not in new content
  • 'append' — Only add new elements, never remove existing
  • 'morph' — Treat head like body (standard element morphing)
  • 'none' — Skip head entirely
HyperMorph.morph(document, newHtml, {
  head: {
    style: 'merge',
    block: true, // wait for CSS to load
    shouldPreserve: (el) => el.id === 'critical-styles',
    afterHeadMorphed: (head, { added, kept, removed }) => {
      console.log(`Added ${added.length}, kept ${kept.length}, removed ${removed.length}`);
    }
  }
});

Scripts Configuration

Control how <script> elements in body are handled. Disabled by default to preserve backwards compatibility.

| Option | Type | Default | Description | |--------|------|---------|-------------| | handle | boolean | false | Enable special script handling | | shouldPreserve | (el) => boolean | Check im-preserve | Keep script even if not in new content | | shouldReAppend | (el) => boolean | Check im-re-append | Force re-execution of existing script | | shouldRemove | (el) => boolean | () => {} | Return false to prevent removal | | afterScriptsHandled | (container, {added, kept, removed}) => void | noop | Called after script processing |

Script behavior when handle: true:

  • Same script exists (matching outerHTML) → Preserved, not re-executed
  • New script → Executed via createContextualFragment
  • External script (src) → Waits for load event before resolving
HyperMorph.morph(el, html, {
  scripts: {
    handle: true,
    shouldReAppend: (el) => el.dataset.reload === 'true',
    afterScriptsHandled: (container, { added }) => {
      console.log('Executed scripts:', added.length);
    }
  }
});

Returns a Promise when scripts need to load:

await HyperMorph.morph(el, html, { scripts: { handle: true } });

HTML Attributes

Control element behavior via HTML attributes:

| Attribute | Effect | |-----------|--------| | im-preserve="true" | Keep element even if removed from new content | | im-re-append="true" | Force re-insertion (re-executes scripts) |

<!-- This script will re-execute on every morph -->
<script im-re-append="true">
  console.log('Re-executed!');
</script>

<!-- This stylesheet persists even if not in new HTML -->
<link rel="stylesheet" href="critical.css" im-preserve="true">

Matching Priority

  1. ID match — Elements with matching id in subtree (Idiomorph's original logic)
  2. HyperMorph — Content-based matching for anonymous elements
  3. Soft match — Same tag/nodeType as fallback

Elements with id attributes are excluded from HyperMorph and handled by ID-based logic.


Scoring Model

| Factor | Score | Description | |--------|-------|-------------| | Signature match | +100 | Required. Same tag + classes + key attributes | | Path segment | +10 each | Matching ancestors (max 4 segments) | | Text match | +20 | Element's text content matches | | Text mismatch | -25 | Text differs or one has text, other doesn't | | Unique candidate | +50 | Only one element with this signature (when text matches) | | Position drift | -1 per | Index difference between old and new position |

Acceptance threshold: ≥ 101 Signature match alone (100) isn't sufficient. Requires at least one additional signal.


Standalone Matcher

Use the matching algorithm independently:

import { createMatcher } from 'hyper-morph/matcher';

const matcher = createMatcher();
const { computeMatches, findMatch, explain } = matcher.session();

// Find all matches between two trees
const matches = computeMatches(oldRoot, newRoot);
// Returns Map<newElement, oldElement>

// Find match for a single element
const match = findMatch(newElement, oldRoot);
if (match) {
  console.log(match.element);     // The matching old element
  console.log(match.confidence);  // Score (101+)
  console.log(match.breakdown);   // Scoring details
}

// Debug why elements do/don't match
const result = explain(newEl, oldEl);
console.log(result.matches);    // boolean
console.log(result.score);      // number
console.log(result.breakdown);  // { signature, path, text, ... }

Matcher Configuration

const matcher = createMatcher({
  includeClasses: true,
  includeAttributes: ['href', 'src', 'name', 'type', 'role', 'aria-label', 'alt', 'title'],
  excludeAttributePrefixes: ['data-morph-', 'data-hyper-', 'data-im-'],
  textHintLength: 64,
  excludeIds: true,
  maxPathDepth: 4,
  landmarks: ['HEADER', 'NAV', 'MAIN', 'ASIDE', 'FOOTER', 'SECTION', 'ARTICLE'],
  weights: {
    signature: 100,
    pathSegment: 10,
    textMatch: 20,
    textMismatch: 25,
    uniqueCandidate: 50,
    positionPenalty: 1,
  },
  minConfidence: 101,
});

DOM APIs Used

  • moveBefore — New DOM API (Chrome 131+, Firefox 133+) that moves elements without triggering lifecycle callbacks. Preserves iframe state, video playback, CSS animations.
  • insertBefore — Fallback for browsers without moveBefore.

Defaults

Access and modify global defaults:

import HyperMorph from 'hyper-morph';

// Read defaults
console.log(HyperMorph.defaults);

// Modify globally
HyperMorph.defaults.morphStyle = 'innerHTML';
HyperMorph.defaults.restoreFocus = false;

Development

npm install
npm test          # Run all tests
npm run dev       # Start demo server at localhost:5692
npm run test:all  # Run tests in all browsers

License

0BSD (Zero-Clause BSD)