Skip to content

Releases: hebus/shikidown

v2.5.0

Choose a tag to compare

@hebus hebus released this 06 Sep 09:33

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_NAMES and DEFAULT_THEME_NAMES, for spreading a subset of the defaults alongside your own additions.
  • MarkdownConfig gains extraLanguages?: LanguageInput[] and extraThemes?: ThemeInput[], typed with Shiki's own @shikijs/core types.
  • languages/theme keep their existing behaviour (replacing, not extending, the default set) for every name already in DEFAULT_LANGUAGE_NAMES/DEFAULT_THEME_NAMES. Only a name outside those lists changes behaviour — it now needs extraLanguages/extraThemes instead of resolving automatically.
  • shiki peer dependency bumped to >=4.4.3.

📦 Install

npm install shikidown shiki markdown-it @angular/elements

Full Changelog: v2.4.1...v2.5.0

v2.4.1

Choose a tag to compare

@hebus hebus released this 09 Aug 13:42

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/elements

Full Changelog: v2.4.0...v2.4.1

v2.4.0

Choose a tag to compare

@hebus hebus released this 09 Aug 13:42

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: MarkdownItPlugin and MarkdownItPluginSource.
  • plugins widens from Array<(md: MarkdownItInstance) => void> to MarkdownItPluginSource[]. 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/elements

Full Changelog: v2.3.0...v2.4.0

v2.3.0

Choose a tag to compare

@hebus hebus released this 09 Aug 13:42

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/elements

Full Changelog: v2.2.0...v2.3.0

v2.2.0

Choose a tag to compare

@hebus hebus released this 08 Aug 13:37

[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-it

Full Changelog: v2.1.0...v2.2.0

v2.1.0

Choose a tag to compare

@hebus hebus released this 08 Aug 13:39

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 irreversiblecustomElements 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 ComponentModule and ComponentModuleSource.
  • 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-it

Full Changelog: v2.0.0...v2.1.0

v2.0.0

Choose a tag to compare

@hebus hebus released this 09 Jul 11:33

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/mermaid isolating the optional mermaid dependency.
  • Workspace upgraded to Angular 22.0.6 / TypeScript 6.0.3 / ng-packagr 22.
  • App is zoneless; rxjs unchanged.

📦 Install

npm install shikidown shiki markdown-it
# optional — only if you render Mermaid diagrams:
npm install mermaid

🔧 Migration from 1.x

  1. Move the Mermaid import to the new subpath (see the diff above).
  2. Ensure your app is on Angular >=22.0.0.