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.
What's the problem?
A plugin passed to
hastPluginsthat 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-katexbefore working out it was never being called.Runnable, no Astro involved:
The first three are indistinguishable from passing
hastPlugins: [].Note that
[rehype transformer ran]never prints.runHastPluginsOnHandledoestypeof 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()walksMETHOD_TO_TYPEfor known visitor names, finds none, returns[], andvisitHastHandlethen 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
validcase above used a bareelement(node, ctx)function and gotMissing field 'tagFilter'— while an entirely wrong kind of plugin is silent.Environment Information
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 atdefineHastPlugin. 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 tohastPlugins.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.