Skip to content

Releases: harlan-zw/mdream

v1.0.3

20 Mar 13:02
Immutable release. Only release title and notes can be modified.
0800fd1

Choose a tag to compare

   🐞 Bug Fixes

    View changes on GitHub

v1.0.2

20 Mar 04:27
Immutable release. Only release title and notes can be modified.
1d3b922

Choose a tag to compare

   🐞 Bug Fixes

    View changes on GitHub

v1.0.0

19 Mar 13:06
Immutable release. Only release title and notes can be modified.
e3fd580

Choose a tag to compare

Mdream v1 ships a native Rust engine, WebAssembly for edge runtimes, and a simpler declarative API while keeping the JS engine available as @mdream/js for users who need imperative hook-based plugins.

👀 Highlights

⚡️ Native Rust Engine

Single-pass architecture via NAPI-RS: parsing and Markdown generation in one traversal, no intermediate DOM.

Input Size mdream (Rust NAPI) mdream (JS) Speedup
166 KB 🏆 0.60ms 3.36ms 5.6×
420 KB 🏆 1.26ms 7.79ms 6.2×
1.8 MB 🏆 7.83ms 62.2ms 7.9×

Native Rust (no Node.js overhead) hits 3.84ms on 1.8MB with PGO, up to ~16× faster than JS.

Install mdream and the platform-specific binary resolves automatically.

🌐 WebAssembly for Edge Runtimes

For Cloudflare Workers, Vercel Edge, and browsers. Export conditions (workerd, edge-light, browser) select the correct build automatically, or use mdream/worker directly.

🔧 Declarative Plugin Config

Built-in plugins take a flat options object instead of an array of factory calls. Serializable, works with both engines.

import { htmlToMarkdown } from 'mdream'

const markdown = htmlToMarkdown(html, { minimal: true })

// Or configure individually
let title = ''
const markdown = htmlToMarkdown(html, {
  frontmatter: (fm) => { title = fm.title },
  filter: { exclude: ['nav', 'footer', 'aside'] },
  extraction: { h1: el => console.log('Title:', el.textContent) },
  tagOverrides: { 'x-code': 'pre' },
})

🧩 @mdream/js: Standalone JS Engine

Custom hook-based plugins (createPlugin, extractionPlugin with callbacks) live here:

import { createPlugin, htmlToMarkdown } from '@mdream/js'

const myPlugin = createPlugin({
  onNodeEnter(el, state) { /* ... */ },
  processTextNode(node, state) { /* ... */ },
})

const markdown = htmlToMarkdown(html, {
  frontmatter: true,
  hooks: [myPlugin],
})

📋 Packages

Package Description
mdream Rust NAPI engine + WASM for edge. Performance-first. Primary package.
@mdream/js Pure JS engine. Full hook access, zero native deps. Subpaths: /plugins, /splitter, /preset/minimal, /parse, /llms-txt, /negotiate.
@mdream/crawl Site-wide crawler for llms.txt generation.
@mdream/rust-* Platform-specific native binaries (auto-resolved).

⚖️ Engine Comparison

mdream is the performance-focused engine. @mdream/js is the pluggability-focused engine with full hook access and zero native dependencies.

Feature mdream (Rust) @mdream/js
Performance ~7.8ms (1.8MB) ~62ms (1.8MB)
Custom hook plugins hooks
Declarative plugins
Streaming
Extraction / Frontmatter
Edge/WASM Built-in JS runs anywhere
parseHtml access

⚠️ Bundler Compatibility

The native Rust engine uses NAPI-RS to load platform-specific binaries at runtime. Bundlers that attempt to statically analyze or bundle .node files (e.g. Turbopack in Next.js 15/16) will fail because the loader pattern uses createRequire() and dynamic path resolution.

Next.js / Turbopack

Add mdream to serverExternalPackages in your next.config.js:

const nextConfig = {
  serverExternalPackages: ['mdream'],
}

This tells Next.js to skip bundling mdream and load it via native require() at runtime, which is required for any package with native Node.js bindings.

Other Bundlers

If your bundler fails to resolve mdream/napi, mark mdream as external in your bundler config. For example, in webpack:

externals: ['mdream']

The @mdream/js package has zero native dependencies and works with all bundlers without any configuration.

💥 Migration from v0.x

Migrate with AI. Copy the prompt below into your AI coding assistant along with any file that imports from mdream:

Migrate from mdream v0.x to v1.0. Rules:
- `htmlToMarkdown()` now returns `string`, not an object. Frontmatter/extraction use callbacks.
- Replace `plugins: [frontmatterPlugin(), ...]` with declarative config: `{ frontmatter: true, isolateMain: true, tailwind: true, filter: { exclude: ['nav'] } }` or `{ minimal: true }`.
- Custom hook plugins (createPlugin, extractionPlugin with callbacks) must import from '@mdream/js' and use `hooks: [...]` instead of `plugins: [...]`.
- Subpath imports moved: mdream/plugins -> @mdream/js/plugins, mdream/splitter -> @mdream/js/splitter, mdream/preset/minimal -> @mdream/js/preset/minimal, mdream/llms-txt -> @mdream/js/llms-txt, mdream/negotiate -> @mdream/js/negotiate.
- Type renames: Plugin -> TransformPlugin (from @mdream/js), HTMLToMarkdownOptions -> MdreamOptions.
- TAG_* constants and ELEMENT_NODE/TEXT_NODE: import from '@mdream/js' instead of 'mdream/plugins'.
- filter.exclude uses string[] ('nav', 'footer') not TAG_* constants when using the Rust engine.

Quick Decision Tree

Custom plugins (createPlugin, extractionPlugin with callbacks)?
  YES -> import from '@mdream/js', use `hooks` option
  NO  -> import from 'mdream', use declarative config

Subpath imports (mdream/plugins, mdream/splitter)?
  YES -> update to @mdream/js equivalents (see table below)
  NO  -> no change needed

Frontmatter or extracted element data?
  YES -> use `frontmatter` callback or `extraction` handlers
  NO  -> no change needed

Breaking Changes

1. Return type: string, not object

htmlToMarkdown() returns a string. Frontmatter and extraction use callbacks.

- const result = htmlToMarkdown(html, { plugins: [frontmatterPlugin()] })
- console.log(result.frontmatter)
+ let frontmatter: Record<string, string> | undefined
+ const markdown = htmlToMarkdown(html, {
+   frontmatter: (fm) => { frontmatter = fm },
+ })

Config object form also works: frontmatter: { onExtract: (fm) => { ... } }

2. Plugin array → declarative config

- import { filterPlugin, frontmatterPlugin, isolateMainPlugin, tailwindPlugin } from 'mdream/plugins'
- htmlToMarkdown(html, {
-   plugins: [frontmatterPlugin(), isolateMainPlugin(), tailwindPlugin(), filterPlugin({ exclude: [TAG_NAV] })]
- })
+ htmlToMarkdown(html, {
+   frontmatter: true,
+   isolateMain: true,
+   tailwind: true,
+   filter: { exclude: ['nav'] }
+ })

Or: { minimal: true }. Both mdream and @mdream/js accept the same config shape.

3. Custom hook plugins → @mdream/js

The Rust engine cannot execute JS callbacks mid-conversion.

- import { createPlugin } from 'mdream/plugins'
- htmlToMarkdown(html, { plugins: [createPlugin({ onNodeEnter(el) { ... } })] })
+ import { htmlToMarkdown, createPlugin } from '@mdream/js'
+ htmlToMarkdown(html, { hooks: [createPlugin({ onNodeEnter(el) { ... } })] })

Passing Plugin[] to the Rust engine throws: Custom hook plugins require @mdream/js.

4. Subpath imports moved

v0.x v1.0
mdream/plugins @mdream/js/plugins
mdream/splitter @mdream/js/splitter
mdream/preset/minimal @mdream/js/preset/minimal
mdream/llms-txt @mdream/js/llms-txt
mdream/negotiate @mdream/js/negotiate

Added: mdream/worker for edge runtimes.

5. Removed from main entry

Removed Use instead
parseHtml @mdream/js
MarkdownProcessor htmlToMarkdown()
TagIdMap @mdream/js
createPlugin @mdream/js/plugins

6. Type renames

- import type { Plugin, HTMLToMarkdownOptions } from 'mdream'
+ import type { TransformPlugin } from '@mdream/js'
+ import type { MdreamOptions } from 'mdream'

7. Constants

- import { ELEMENT_NODE, TAG_NAV } from 'mdream/plugins'
+ import { ELEMENT_NODE, TAG_H1 } from '@mdream/js'

Only TAG_H1-TAG_H6, ELEMENT_NODE, and TEXT_NODE are exported. Use string names ('nav', 'footer') in filter config.

8. filter.exclude types

  • mdream (Rust): string[] only
  • @mdream/js: (string | number)[] (tag names or TAG_* constants)

String tag names work everywhere: { filter: { exclude: ['nav', 'footer'] } }

Changelog

   🚀 Features

   🐞 Bug Fixes

Read more

v1.0.0-beta.14

18 Mar 15:48
Immutable release. Only release title and notes can be modified.
f3e243f

Choose a tag to compare

v1.0.0-beta.14 Pre-release
Pre-release

No significant changes

    View changes on GitHub

v1.0.0-beta.13

18 Mar 15:37
Immutable release. Only release title and notes can be modified.
d4c02bd

Choose a tag to compare

v1.0.0-beta.13 Pre-release
Pre-release

   🐞 Bug Fixes

  • parser: Handle < operator inside script tags without breaking parse loop  -  by @harlan-zw in #66 (5fda6)
    View changes on GitHub

v1.0.0-beta.12

18 Mar 15:37
Immutable release. Only release title and notes can be modified.
cde9b93

Choose a tag to compare

v1.0.0-beta.12 Pre-release
Pre-release

   🚀 Features

   🐞 Bug Fixes

    View changes on GitHub

v1.0.0-beta.11

18 Mar 06:22
Immutable release. Only release title and notes can be modified.
3b5b945

Choose a tag to compare

v1.0.0-beta.11 Pre-release
Pre-release

   🐞 Bug Fixes

   🏎 Performance

    View changes on GitHub

v1.0.0-beta.9

17 Mar 11:55
Immutable release. Only release title and notes can be modified.
899316f

Choose a tag to compare

v1.0.0-beta.9 Pre-release
Pre-release

No significant changes

    View changes on GitHub

v1.0.0-beta.7

17 Mar 11:10
Immutable release. Only release title and notes can be modified.
49c4893

Choose a tag to compare

v1.0.0-beta.7 Pre-release
Pre-release

No significant changes

    View changes on GitHub

v1.0.0-beta.6

17 Mar 11:00
Immutable release. Only release title and notes can be modified.
e89bb25

Choose a tag to compare

v1.0.0-beta.6 Pre-release
Pre-release

No significant changes

    View changes on GitHub