The release of version 0.9.0 of Citation.js comes shortly after the release of version 0.8.0. This is in part because v0.8.0 was released shortly before Node.js v20 went End Of Life, which led to a lot of (developmental) dependencies to dropping support for it. An added benefit though is that v22.12.0 of Node.js finally added support for CommonJS modules (require() syntax) to import most ESM modules (import/export syntax). This allowed me to stop using the @larsgw/wikibase-sdk fork of wikibase-sdk.

Melangyna pavlovskyi, 2026.iii.22, Zeddam, The Netherlands
In short:
- The minimum Node.js version is now v22.12.0.
- Use of
node-fetchwas replaced with the built-in version of the Fetch API (based onundici); similarlysync-fetchwas replaced withsync-fetch-undici. - Use of the fork
@larsgw/wikibase-sdkwas replaced with the upstreamwikibase-sdk. - Babel was updated to v8, which does mean a fork of Babelify had to be used for the browser bundles in the
citation-jspackage, until Babelify is updated upstream. - The repository documentation now explicitly directs contributors to not include spurious
Co-Authored-Bytags in their commit messages for advertising purposes (i.e. LLMs).
Another major change is the addition of TypeScript types, which people have been asking for, for some time. I have attempted to create type definitions that are actually useful, which proved difficult given the convoluted plugin system. In the process, I learnt some interesting features of TypeScript, like module augmentation and some pretty nice generic functions.
Module augmentation
The way to register a plugin with Citation.js is to import or require() it. The first step was to employ module augmentation to add these overloads, like below. (Note that some formats have multiple return types, meaning the overload also depends on the second argument; this will come back later!)
// node_modules/@citation-js/core/index.d.ts
export class Cite {
// ...
format (format: 'data', options?: { format?: 'text', version: string }): string
format (format: 'data', options: { format: 'object', version: string }): Array<CSL>
format (format: 'label'): Record<string, string>
}
// node_modules/@citation-js/plugin-csl/index.d.ts
import '@citation-js/core'
declare module '@citation-js/core' {
class Cite {
format (format: 'bibliography', options?: { /* ... */ }): string
// ...
}
}
// index.ts
import { Cite } from '@citation-js/core'
import from '@citation-js/plugin-csl'
const a = (new Cite()).format('bibliography')
// typeof a === string
This is for plugins with output formatting features, for input parsing features and plugin configuration something similar is possible. However, formatting features not only affect the possible overloads of the Cite#format() function, but also the plugins.output.format() function. This would mean repeating all the overloads.
Additionally, if the typing of a call to Cite#format() is wrong, the error is actually not very helpful. TypeScript will only show how the last overload of a function does not match. There is another way, though.
Generic functions
With generic functions for Cite#format() and plugins.output.format() we can reuse the same output formatter signatures for both functions. This was achieved by exporting an interface type with the signatures.
interface Formats {
data:
| ((options?: { format?: 'text', version?: string }) => string)
| ((options: { format: 'object', version?: string }) => Array<CSL>)
label: () => Record<string, string>
}
This interface can be extended by plugins as well, so adding signatures is simple. The generic function is not so simple; I started with the following:
export class Cite {
format<
Format extends keyof Formats,
Options extends Parameters<Formats[Format]>
> (format: Format, ...options: Options): ReturnType<Formats[Format]>
}
However, because the 'data' output format can have multiple return types depending on the second argument, the return type would be string | Array<CSL>. I instead ended up with
export class Cite {
format<
Format extends keyof Formats,
Options extends Parameters<Formats[Format]>,
Result extends ReturnType<Extract<Formats[Format], (...args: Options) => any>>
> (format: Format, ...options: Options): Result
}
Because of Extract<T> the correct return type is selected and the overloads work as expected. This works wonders with autocomplete, and gives appropriate errors. And, although the resulting code looks pretty simple, it took a while to get there in practice.
Type definitions for plugins
In total, the index.d.ts type definitions for a typical plugin looks like this:
import type { CSL } from '@citation-js/core'
interface Entry {
title: string
// ...
}
interface Config {
stringConstants: Record<string, string>
}
declare module '@citation-js/core' {
namespace plugins {
namespace input {
interface Formats {
'@foo/file': (input: string) => Array<Entry>
'@foo/record': (input: Entry) => CSL
}
}
namespace output {
interface Formats {
foo:
| ((options: { format: 'object', spec?: number }) => Array<Entry>)
| ((options?: { format?: 'text', spec?: number }) => string)
}
}
namespace config {
function get ('@foo'): Config
}
}
}
This is implemented for all plugins included in citation-js (though that package does not have TypeScript types itself), and I will slowly roll this out for other plugins. Any feedback is welcome!












