Skip to content

hastPlugins silently ignores entries that expose no visitors (e.g. a rehype plugin) #180

Description

@ChristineTham

What's the problem?

A plugin passed to hastPlugins that isn't a valid Sätteri plugin definition is accepted and then does nothing. There's no warning, no runtime type error, and the build succeeds — the only symptom is that the transform never happened.

This is easy to hit while migrating from unified, because a rehype plugin is a plausible thing to try first. I lost a while to rehype-katex before working out it was never being called.

Runnable, no Astro involved:

import { markdownToHtml, defineHastPlugin } from 'satteri'

const md = 'Inline $E = mc^2$.\n'
const rehypeLike = () => (tree) => { console.log('[rehype transformer ran]'); return tree }

const valid = defineHastPlugin({
  name: 'marker',
  element: { filter: ['code'], visit(node, ctx) { ctx.setProperty(node, 'data-seen', 'yes') } }
})

for (const [label, hastPlugins] of [
  ['bare rehype-style plugin', [rehypeLike]],
  ['[plugin, options] tuple',  [[rehypeLike, { strict: false }]]],
  ['plain object, no visitors',[{ name: 'nothing' }]],
  ['valid Sätteri plugin',     [valid]]
]) {
  const { html } = await markdownToHtml(md, { features: { math: true }, hastPlugins })
  console.log(label, '->', html.trim())
}
bare rehype-style plugin  -> <p>Inline <code class="language-math math-inline">E = mc^2</code>.</p>
[plugin, options] tuple   -> <p>Inline <code class="language-math math-inline">E = mc^2</code>.</p>
plain object, no visitors -> <p>Inline <code class="language-math math-inline">E = mc^2</code>.</p>
valid Sätteri plugin      -> <p>Inline <code class="language-math math-inline" data-seen="yes">E = mc^2</code>.</p>

The first three are indistinguishable from passing hastPlugins: [].

Note that [rehype transformer ran] never prints. runHastPluginsOnHandle does typeof raw === "function" ? raw() : raw, so a bare unified plugin is invoked as a factory, returns its transformer, and the transformer is then discarded because it has no visitor methods. resolveSubscriptions() walks METHOD_TO_TYPE for known visitor names, finds none, returns [], and visitHastHandle then walks with zero subscriptions.

Worth noting the failure mode is inverted from what's useful: a malformed Sätteri plugin fails loudly and helpfully — my first attempt at the valid case above used a bare element(node, ctx) function and got Missing field 'tagFilter' — while an entirely wrong kind of plugin is silent.

Environment Information

  • satteri npm v0.9.5
  • Node.js v24.16.0
  • macOS (Darwin 25.5.0)
  • Not built from source; no local Rust toolchain involved

What's the expected result?

A warning when a plugin resolves to zero subscriptions, e.g. plugin "marker" subscribes to no node types and will not run. That would have turned this into a ten-second fix.

Optionally, a more specific message when an entry looks like a unified plugin (a function returning a function, or a [fn, options] tuple), pointing at defineHastPlugin. This seems likely to become a common migration mistake now that Astro 7 ships Sätteri as its default processor and directs users to convert rehype plugins to hastPlugins.

I see #170 (allow warnings to be silenced), so there is presumably existing warning infrastructure this could hang off.

Not a duplicate of #178 (native MathML rendering) — this is about plugin validation, and applies equally to mdastPlugins.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions