Releases: hebus/shikidown
Release list
v2.5.0
Shiki now loads only the languages and themes it needs instead of statically referencing all ~235 languages and ~64 themes it ships — and your app can add its own on top.
📦 Smaller bundles by default
createHighlighter — the entry point the library used to import from shiki — statically references every bundled grammar and theme, each behind its own import(). A bundler has to know about all ~235 languages and ~64 themes at build time, regardless of the handful your app actually configures. Every consumer of shikidown was paying for hundreds of unused lazy chunks.
The library now imports createHighlighterCore and an explicit oniguruma engine instead, with a lazy loader per language/theme for the existing default set (the same 16 languages as before, plus the 4 themes already used across the demo and docs: github-dark, github-light, poimandres, catppuccin-latte). Nothing else is referenced statically.
In the demo app, this cut the number of generated lazy chunks from ~360 down to ~75, with no change to the initial bundle size — the reduction is entirely in chunks that were never downloaded unless requested, but that still bloated every consumer's build graph.
✨ extraLanguages and extraThemes
Anything outside the default set — php, dracula, Angular's own angular-ts/angular-html, or any of Shiki's other ~230 languages and ~60 themes — previously worked out of the box, because the full bundle had it whether you asked for it or not. Now it has to be imported explicitly:
provideMarkdown({
languages: ['typescript'],
extraLanguages: [() => import('@shikijs/langs/go')],
theme: 'dracula',
extraThemes: [() => import('@shikijs/themes/dracula')],
});Use the () => import(…) loader form, not a static import: provideMarkdown() is typically called eagerly from app.config.ts, so a static import would land the grammar or theme back in your initial bundle instead of a chunk loaded on demand.
A fence whose language isn't loaded still renders unhighlighted rather than failing — same fallback as before.
🔧 API
- New exported constants:
DEFAULT_LANGUAGE_NAMESandDEFAULT_THEME_NAMES, for spreading a subset of the defaults alongside your own additions. MarkdownConfiggainsextraLanguages?: LanguageInput[]andextraThemes?: ThemeInput[], typed with Shiki's own@shikijs/coretypes.languages/themekeep their existing behaviour (replacing, not extending, the default set) for every name already inDEFAULT_LANGUAGE_NAMES/DEFAULT_THEME_NAMES. Only a name outside those lists changes behaviour — it now needsextraLanguages/extraThemesinstead of resolving automatically.shikipeer dependency bumped to>=4.4.3.
📦 Install
npm install shikidown shiki markdown-it @angular/elementsFull Changelog: v2.4.1...v2.5.0
v2.4.1
Fixes lazy plugin loading, which was broken in production builds in v2.4.0.
🐛 Lazy loaders resolved to the wrong object
A plugin declared as { load: () => import('…') } threw during initialisation under a production bundle:
TypeError: s is not a function
at MarkdownService._init
If you adopted { load } in 2.4.0 with a CommonJS plugin, you are affected. A dev server is not: this only surfaces once built and deployed.
What went wrong
How many default wrappers arrive is decided by the bundler, not by the plugin. When esbuild puts a CommonJS package in its own lazy chunk, the chunk's default export is the module.exports object itself — which carries a default of its own:
Te.default = As; ... var chunk = Ts(); export { chunk as default }A single unwrap therefore yields that object rather than the function. A dev server that prebundles its dependencies applies the interop and leaves a single wrapper, which is why the two disagreed.
The library now unwraps until a function turns up, whatever the depth, and throws a message naming what was resolved when none does — the previous failure surfaced as a bare TypeError several frames from its cause.
No API change: { load: () => import('…') } is written exactly as before.
📦 Install
npm install shikidown shiki markdown-it @angular/elementsFull Changelog: v2.4.0...v2.4.1
v2.4.0
Warning
Superseded by v2.4.1. The lazy loader introduced here resolves to the wrong object under a production bundle, throwing is not a function during initialisation. A dev server is unaffected, so the failure only shows up once deployed. Install 2.4.1 or later.
plugins accepts lazy loaders, so a heavy plugin no longer has to sit in your initial bundle.
✨ Lazy plugin loading
A markdown-it plugin had to be imported eagerly to be passed to provideMarkdown(). A heavy one — KaTeX is around 266 kB — therefore landed in the initial bundle of every page, formulas or not.
plugins now also accepts a loader:
provideMarkdown({
plugins: [markdownItAnchor, { load: () => import('@vscode/markdown-it-katex') }],
});Loaders resolve in parallel while the service initialises, then apply in array order — position in the list decides precedence, not which download finished first. The default export of a module is unwrapped for you.
In the demo application, moving KaTeX to this form took main from 697 kB to 405 kB.
🔧 API
- New exported types:
MarkdownItPluginandMarkdownItPluginSource. pluginswidens fromArray<(md: MarkdownItInstance) => void>toMarkdownItPluginSource[]. Existing arrays of plain functions keep working untouched.
Why the lazy form is an object
componentModules accepts a bare () => import('…'), because there a module is an object and a loader a function — typeof tells them apart. A plugin is itself a function, so nothing distinguishes it from a loader, not even arity: a plugin may legitimately ignore its argument and report an arity of 0. The load key removes the guesswork rather than relying on a heuristic in a public API.
📦 Install
npm install shikidown shiki markdown-it @angular/elementsFull Changelog: v2.3.0...v2.4.0
v2.3.0
markdown-it 15 support, and two peer dependencies that were missing from the published package.
🐛 markdown-it 15
shikidown declares markdown-it: >=14.0.0, yet it did not compile against v15. In v14 the default export is a class — a name that is both a value and a type. In v15 it is a callable const:
declare class MarkdownIt { ... }
declare const MarkdownItCallable: typeof MarkdownIt & ((...) => MarkdownIt);
export { type MarkdownIt, ..., MarkdownItCallable as default };The default import therefore resolves to a plain value, and using it in type position fails with TS2749. The library now goes through InstanceType<typeof MarkdownIt>, the one shape both majors expose.
No action needed if you are staying on v14. Moving to v15 makes @types/markdown-it redundant — the declarations ship inside the package.
🐛 Missing peer dependencies
@angular/elements and @angular/platform-browser are now declared. They were absent from 2.2.0 on npm, even though provideMarkdown() imports createCustomElement from @angular/elements statically.
This is the most concrete reason to upgrade: nothing warned you when the package was missing, and ng new does not add it to your package.json despite it shipping with Angular.
npm install @angular/elements🔧 API
- New exported type:
MarkdownItInstance— use it to type your own plugins instead of markdown-it's default export.
import type { MarkdownItInstance } from 'shikidown';
function myPlugin(md: MarkdownItInstance): void {
md.use(/* … */);
}No breaking change: existing public signatures are unchanged.
⚠️ TypeScript prerequisite
shiki >=4.4 declares [Symbol.dispose]() on its highlighter, which the ES2022 library does not know about. Without it the build fails before it reaches your own code:
TS2550: Property 'dispose' does not exist on type 'SymbolConstructor'.
Add ESNext.Disposable to lib in your tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable", "ESNext.Disposable"]
}
}Prefer this over "lib": ["esnext"], which would also enable every other proposal-stage API.
🧭 Migrating from markdown-it 14
markdown-it 15 ships linkify-it v6, which disables fuzzy links by default: example.com without a protocol is no longer turned into a link. Since shikidown enables linkify: true by default, you will see the difference. It stays under your control:
provideMarkdown({ markdownOptions: { linkify: false } });📚 Documentation
The official documentation site is live: https://hebus.github.io/shikidown/docs/
📦 Install
npm install shikidown shiki markdown-it @angular/elementsFull Changelog: v2.2.0...v2.3.0
v2.2.0
[componentModules] now defines only the components the document actually uses.
✨ Selector filtering
A page module usually exports more than its Markdown tags — a dialog mounted imperatively, a host component reused elsewhere. Defining those is not neutral: customElements.define is global and retroactive, so the browser upgrades any element bearing a defined tag as soon as it enters the DOM. A component that other code creates with createComponent() and appends to the body would be instantiated a second time, outside the injection context its creator set up — and typically fail on whatever that context provided. A callable dialog reading its arguments from an injected handle simply never opens, with nothing pointing back at the registration.
The [componentModules] input now tests each selector against the rendered content and skips the ones that never appear:
<shikidown [content]="md" [componentModules]="[() => import('./demos')]" />The effect re-runs when the content changes, so a selector that shows up later is still registered.
This does not apply to provideMarkdown({ componentModules }), which has no document to look at and still registers everything it is given.
🔧 API
selectorUsedIn(content, selector)is exported, if you need the same test elsewhere.registerComponentModules(sources, injector, isUsed?)takes an optional predicate as a third argument to filter with your own rule. Existing two-argument calls are unchanged and register everything.
No breaking change: the public signatures are backwards compatible.
📦 Install
npm install shikidown shiki markdown-itFull Changelog: v2.1.0...v2.2.0
v2.1.0
Register components by module instead of by selector, with optional lazy loading. Purely additive.
✨ componentModules
components requires one selector → class pair per component, which is tedious past a handful and redundant — the selector is already declared in the @Component decorator. Pass the module instead and let shikidown read the selectors through reflectComponentType():
import * as demos from './demos';
provideMarkdown({ componentModules: [demos] })Accepted by both provideMarkdown() and the <shikidown> input. Exports that are not components are ignored, so a module can freely export mock data, helper functions or attribute-selector directives alongside its components.
🚀 Lazy loading
An entry can be a function returning a dynamic import, so the module stays out of the initial bundle and is fetched only when the page that needs it is displayed. This is what keeps a documentation site with sixty pages from shipping all sixty pages' components to every visitor:
export const DEMO_LOADERS: Record<string, ComponentModuleSource[]> = {
button: [() => import('./demos/button.demos')],
tag: [() => import('./demos/tag.demos')],
};<shikidown [content]="content()" [componentModules]="modules()" />Registering after the markdown is rendered still works. Custom element upgrades are retroactive by specification: when customElements.define() runs, the browser walks the document and upgrades any matching element already in the DOM. An unknown <my-counter> sitting inertly in freshly rendered HTML comes alive as soon as its definition lands — no ordering constraint, and attributes present before registration are not lost (attributeChangedCallback fires during the upgrade).
Note that registration is irreversible — customElements has no undefine. Pass an injector whose lifetime is at least as long as the page.
🔧 API
registerComponentModules(sources, injector)is exported for imperative use, is idempotent, SSR-safe, and returns a promise so callers can await registration.- New types
ComponentModuleandComponentModuleSource. - Selectors that cannot be custom element names are skipped rather than thrown on.
No breaking change: components and registerAsCustomElement are untouched.
📦 Install
npm install shikidown shiki markdown-itFull Changelog: v2.0.0...v2.1.0
v2.0.0
First major release. Two breaking changes are bundled together.
⚠️ Breaking changes
Mermaid moved to a dedicated entry point
MermaidDirective is no longer exported from the package root — import it from shikidown/mermaid:
- import { MarkdownComponent, MermaidDirective } from 'shikidown';
+ import { MarkdownComponent } from 'shikidown';
+ import { MermaidDirective } from 'shikidown/mermaid';No template or API change (<shikidown mermaid [content]="…" /> is unchanged). As a result, consumers who don't render diagrams no longer pull mermaid into their dependency graph: the primary FESM bundle now contains zero import('mermaid'). mermaid remains an optional peer dependency, loaded lazily only when the directive runs.
Angular 22 required
The library is now built with the Angular 22 partial compiler and TypeScript 6.0. The @angular/common and @angular/core peer dependencies were bumped to >=22.0.0 (consumers need the v22 linker to consume the package).
✨ Highlights
- New ng-packagr secondary entry point
shikidown/mermaidisolating the optionalmermaiddependency. - Workspace upgraded to Angular 22.0.6 / TypeScript 6.0.3 / ng-packagr 22.
- App is zoneless;
rxjsunchanged.
📦 Install
npm install shikidown shiki markdown-it
# optional — only if you render Mermaid diagrams:
npm install mermaid🔧 Migration from 1.x
- Move the Mermaid import to the new subpath (see the diff above).
- Ensure your app is on Angular
>=22.0.0.