From 659730a43ce5561558fbe0ee5ce75ccc5bfdbf94 Mon Sep 17 00:00:00 2001 From: Titus Date: Sat, 18 Jun 2022 17:19:36 +0200 Subject: [PATCH 01/46] Remove reference to `unified-engine-atom` --- readme.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 59eb796c..c1311510 100644 --- a/readme.md +++ b/readme.md @@ -279,8 +279,7 @@ with the [`data`][data] method. **unified** can integrate with the file system with [`unified-engine`][engine]. CLI apps can be created with [`unified-args`][args], Gulp plugins with -[`unified-engine-gulp`][gulp], and Atom Linters with -[`unified-engine-atom`][atom]. +[`unified-engine-gulp`][gulp]. [`unified-stream`][stream] provides a streaming interface. @@ -1335,8 +1334,6 @@ work on [`ware`][ware], as it was a huge initial inspiration. [gulp]: https://github.com/unifiedjs/unified-engine-gulp -[atom]: https://github.com/unifiedjs/unified-engine-atom - [remark-rehype]: https://github.com/remarkjs/remark-rehype [remark-retext]: https://github.com/remarkjs/remark-retext From 0ee591d21e113af6cc70c04c5104280581901fe8 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 19 Jun 2022 12:17:28 +0200 Subject: [PATCH 02/46] Update dev-dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4be454f0..5da980f8 100644 --- a/package.json +++ b/package.json @@ -63,10 +63,10 @@ "remark-preset-wooorm": "^9.0.0", "rimraf": "^3.0.0", "tape": "^5.0.0", - "tsd": "^0.19.0", + "tsd": "^0.21.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", - "xo": "^0.48.0" + "xo": "^0.50.0" }, "scripts": { "build": "rimraf \"test/**/*.d.ts\" && tsc && tsd && type-coverage", From c5ff1279ce76a384cfa1f4a2a3be6a59542d48e7 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 19 Jun 2022 12:17:31 +0200 Subject: [PATCH 03/46] Fix type --- lib/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index ef5748e6..c255c960 100644 --- a/lib/index.js +++ b/lib/index.js @@ -38,7 +38,7 @@ function base() { const transformers = trough() /** @type {Processor['attachers']} */ const attachers = [] - /** @type {Record} */ + /** @type {{settings?: Record} & Record} */ let namespace = {} /** @type {boolean|undefined} */ let frozen @@ -178,7 +178,7 @@ function base() { return processor /** - * @param {import('..').Pluggable} value + * @param {import('..').Pluggable>} value * @returns {void} */ function add(value) { From 2aa15eaccf00c9b538e0f8be524080f4330d4315 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 19 Jun 2022 12:22:09 +0200 Subject: [PATCH 04/46] Refactor types --- lib/index.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/index.js b/lib/index.js index c255c960..925c4dfb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,20 +2,16 @@ * @typedef {import('unist').Node} Node * @typedef {import('vfile').VFileCompatible} VFileCompatible * @typedef {import('vfile').VFileValue} VFileValue - * @typedef {import('..').Processor} Processor - * @typedef {import('..').Plugin} Plugin - * @typedef {import('..').Preset} Preset - * @typedef {import('..').Pluggable} Pluggable - * @typedef {import('..').PluggableList} PluggableList - * @typedef {import('..').Transformer} Transformer - * @typedef {import('..').Parser} Parser - * @typedef {import('..').Compiler} Compiler - * @typedef {import('..').RunCallback} RunCallback - * @typedef {import('..').ProcessCallback} ProcessCallback - * - * @typedef Context - * @property {Node} tree - * @property {VFile} file + * @typedef {import('../index.js').Processor} Processor + * @typedef {import('../index.js').Plugin} Plugin + * @typedef {import('../index.js').Preset} Preset + * @typedef {import('../index.js').Pluggable} Pluggable + * @typedef {import('../index.js').PluggableList} PluggableList + * @typedef {import('../index.js').Transformer} Transformer + * @typedef {import('../index.js').Parser} Parser + * @typedef {import('../index.js').Compiler} Compiler + * @typedef {import('../index.js').RunCallback} RunCallback + * @typedef {import('../index.js').ProcessCallback} ProcessCallback */ import {bail} from 'bail' @@ -178,7 +174,7 @@ function base() { return processor /** - * @param {import('..').Pluggable>} value + * @param {import('../index.js').Pluggable>} value * @returns {void} */ function add(value) { From 144eec01b0e1faa23655359d61c3749ad2af99e7 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 19 Jun 2022 17:21:19 +0200 Subject: [PATCH 05/46] Add improved docs --- package.json | 6 +- readme.md | 1260 +++++++++++++++++++++++++++----------------------- 2 files changed, 699 insertions(+), 567 deletions(-) diff --git a/package.json b/package.json index 5da980f8..797e32ec 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,11 @@ }, "remarkConfig": { "plugins": [ - "preset-wooorm" + "preset-wooorm", + [ + "remark-lint-no-html", + false + ] ] }, "typeCoverage": { diff --git a/readme.md b/readme.md index c1311510..987cfdc3 100644 --- a/readme.md +++ b/readme.md @@ -8,112 +8,99 @@ [![Backers][backers-badge]][collective] [![Chat][chat-badge]][chat] -**unified** is an interface for processing text using syntax trees. -It’s what powers [**remark**][remark] (Markdown), [**retext**][retext] (natural -language), and [**rehype**][rehype] (HTML), and allows for processing between -formats. - -## Intro - -**unified** enables new exciting projects like [Gatsby][] to pull in Markdown, -[MDX][] to embed [JSX][], and [Prettier][] to format it. -It’s used in about 700k projects on GitHub and has about 35m downloads each -month on npm: you’re probably using it. -Some notable users are [Node.js][], [Vercel][], [Netlify][], [GitHub][], -[Mozilla][], [WordPress][], [Adobe][], [Facebook][], [Google][], and many more. - -* To read about what we are up to, follow us [Twitter][] -* For a less technical and more practical introduction to unified, visit - [`unifiedjs.com`][site] and peruse its [Learn][] section -* Browse [awesome unified][awesome] to find out more about the ecosystem -* Questions? - Get help on [Discussions][chat]! -* Check out [Contribute][] below to find out how to help out, or become a - backer or sponsor on [OpenCollective][collective] - -## Sponsors +**unified** lets you inspect and transform content with plugins. -Support this effort and give back by sponsoring on [OpenCollective][collective]! +## Contents - +* [What is this?](#what-is-this) +* [When should I use this?](#when-should-i-use-this) +* [Install](#install) +* [Use](#use) +* [Overview](#overview) +* [API](#api) + * [`processor()`](#processor) + * [`processor.use(plugin[, options])`](#processoruseplugin-options) + * [`processor.parse(file)`](#processorparsefile) + * [`processor.stringify(tree[, file])`](#processorstringifytree-file) + * [`processor.run(tree[, file][, done])`](#processorruntree-file-done) + * [`processor.runSync(tree[, file])`](#processorrunsynctree-file) + * [`processor.process(file[, done])`](#processorprocessfile-done) + * [`processor.processSync(file)`](#processorprocesssyncfile) + * [`processor.data([key[, value]])`](#processordatakey-value) + * [`processor.freeze()`](#processorfreeze) +* [`Plugin`](#plugin) + * [`function attacher(options?)`](#function-attacheroptions) + * [`function transformer(tree, file[, next])`](#function-transformertree-file-next) +* [`Preset`](#preset) +* [Types](#types) +* [Compatibility](#compatibility) +* [Contribute](#contribute) +* [Sponsor](#sponsor) +* [Acknowledgments](#acknowledgments) +* [License](#license) - - - - - - - - - - - - - - - - - - - - - - - -
- Vercel

- -
- Motif

- -
- HashiCorp

- -
- American Express

- -
- GitBook

- -
- Gatsby

- -
- Netlify

- - -
- Coinbase

- -
- ThemeIsle

- -
- Expo

- -
- Boost Note

- -
- Holloway

- -
-
- You? -

-
+## What is this? -## Install +unified is two things: + +* **unified** is a collective of 500+ free and open source packages that work + with content as structured data (ASTs) +* `unified` (this project) is the core package, used in 800k+ projects on GH, + to process content with plugins -This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): -Node 12+ is needed to use it and it must be `import`ed instead of `require`d. +Several ecosystems are built on unified around different kinds of content. +Notably, [remark][] (markdown), [rehype][] (HTML), and [retext][] (natural +language). +These ecosystems can be connected together. -[npm][]: +* for more about us, see [`unifiedjs.com`][site] +* for updates, see [@unifiedjs][twitter] on Twitter +* for questions, see [support][] +* to help, see [contribute][] and [sponsor][] below + +## When should I use this? + +In some cases, you are already using unified. +For example, it’s used in MDX, Gatsby, Docusaurus, etc. +In those cases, you don’t need to add `unified` yourself but you can include +plugins into those projects. + +But the real fun (for some) is to get your hands dirty and work with syntax +trees and build with it yourself. +You can create those projects, or things like Prettier, or your own site +generator. +You can connect utilities together and make your own plugins that check for +problems and transform from one thing to another. + +When you are dealing with one type of content (such as markdown), it’s +recommended to use the main package of that ecosystem instead (so `remark`). +When you are dealing with different kinds of content (such as markdown and +HTML), it’s recommended to use `unified` itself, and pick and choose the plugins +you need. + +## Install + +This package is [ESM only][esm]. +In Node.js (version 12.20+, 14.14+, 16.0+, 18.0+), install with [npm][]: ```sh npm install unified ``` +In Deno with [`esm.sh`][esmsh]: + +```js +import {unified} from 'https://esm.sh/unified@10' +``` + +In browsers with [`esm.sh`][esmsh]: + +```html + +``` + ## Use ```js @@ -125,23 +112,16 @@ import rehypeFormat from 'rehype-format' import rehypeStringify from 'rehype-stringify' import {reporter} from 'vfile-reporter' -unified() +const file = await unified() .use(remarkParse) .use(remarkRehype) .use(rehypeDocument, {title: '👋🌍'}) .use(rehypeFormat) .use(rehypeStringify) .process('# Hello world!') - .then( - (file) => { - console.error(reporter(file)) - console.log(String(file)) - }, - (error) => { - // Handle your error here! - throw error - } - ) + +console.error(reporter(file)) +console.log(String(file)) ``` Yields: @@ -164,37 +144,19 @@ no issues found ``` -## Contents + -* [Description](#description) -* [API](#api) - * [`processor()`](#processor) - * [`processor.use(plugin[, options])`](#processoruseplugin-options) - * [`processor.parse(file)`](#processorparsefile) - * [`processor.stringify(node[, file])`](#processorstringifynode-file) - * [`processor.run(node[, file][, done])`](#processorrunnode-file-done) - * [`processor.runSync(node[, file])`](#processorrunsyncnode-file) - * [`processor.process(file[, done])`](#processorprocessfile-done) - * [`processor.processSync(file|value)`](#processorprocesssyncfilevalue) - * [`processor.data([key[, value]])`](#processordatakey-value) - * [`processor.freeze()`](#processorfreeze) -* [`Plugin`](#plugin) - * [`function attacher([options])`](#function-attacheroptions) - * [`function transformer(node, file[, next])`](#function-transformernode-file-next) -* [`Preset`](#preset) -* [Contribute](#contribute) -* [Acknowledgments](#acknowledgments) -* [License](#license) + -## Description +## Overview -**unified** is an interface for processing text using syntax trees. -Syntax trees are a representation of text understandable to programs. -Those programs, called [*plugin*][plugin]s, take these trees and inspect and +`unified` is an interface for processing content with syntax trees. +Syntax trees are a representation of content understandable to programs. +Those programs, called *[plugins][plugin]*, take these trees and inspect and modify them. -To get to the syntax tree from text, there is a [*parser*][parser]. -To get from that back to text, there is a [*compiler*][compiler]. -This is the [*process*][process] of a *processor*. +To get to the syntax tree from text, there is a *[parser][]*. +To get from that back to text, there is a *[compiler][]*. +This is the *[process][]* of a *processor*. ```ascii | ........................ process ........................... | @@ -212,7 +174,29 @@ Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output ###### Processors -Every **processor** implements another processor. +Processors process content. +On its own, `unified` (the root processor) doesn’t work. +It needs to be configured with plugins to work. +For example: + +```js +const processor = unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeDocument, {title: '👋🌍'}) + .use(rehypeFormat) + .use(rehypeStringify) +``` + +That processor can do different things. +It can: + +* …parse markdown (`parse`) +* …turn parsed markdown into HTML and format the HTML (`run`) +* …compile HTML (`stringify`) +* …do all of the above (`process`) + +Every processor implements another processor. To create a processor, call another processor. The new processor is configured to work the same as its ancestor. But when the descendant processor is configured in the future it does not affect @@ -221,72 +205,98 @@ the ancestral processor. When processors are exposed from a module (for example, `unified` itself) they should not be configured directly, as that would change their behavior for all module users. -Those processors are [*frozen*][freeze] and they should be called to create a +Those processors are *[frozen][freeze]* and they should be called to create a new processor before they are used. -###### Syntax trees +###### File + +When processing a document, metadata is gathered about that document. +[`vfile`][vfile] is the file format that stores data, metadata, and messages +about files for unified and plugins. -The **syntax trees** used in **unified** are [**unist**][unist] nodes. -A [**node**][node] is a plain JavaScript objects with a `type` field. -The semantics of nodes and format of syntax trees is defined by other projects. +There are several [utilities][vfile-utilities] for working with these files. -There are several [*utilities*][unist-utilities] for working with nodes. +###### Syntax tree -* [**esast**][esast] — JS -* [**hast**][hast] — HTML -* [**mdast**][mdast] — Markdown -* [**nlcst**][nlcst] — Natural language -* [**xast**][xast] — XML +The syntax trees used in unified are [unist][] nodes. +A tree represents a whole document and each [node][] is a plain JavaScript +object with a `type` field. +The semantics of nodes and the format of syntax trees is defined by other +projects: -###### List of processors +* [esast][] — JavaScript +* [hast][] — HTML +* [mdast][] — markdown +* [nlcst][] — natural language +* [xast][] — XML -The following projects process different [*syntax tree*][syntax-tree] formats. -They parse text to a syntax tree and compile that back to text. -These processors can be used as is, or their parser and compiler can be mixed -and matched with **unified** and plugins to process between different syntaxes. +There are many utilities for working with trees listed in each aforementioned +project and maintained in the [`syntax-tree`][syntax-tree] organization. +These utilities are a level lower than unified itself and are building blocks +that can be used to make plugins. -* [**rehype**][rehype] ([*hast*][hast]) — HTML -* [**remark**][remark] ([*mdast*][mdast]) — Markdown -* [**retext**][retext] ([*nlcst*][nlcst]) — Natural language + -###### List of plugins + -The below [**plugins**][plugin] work with **unified**, on all [*syntax -tree*][syntax-tree] formats: +###### Ecosystems -* [`unified-diff`](https://github.com/unifiedjs/unified-diff) - — Ignore messages for unchanged lines in Travis -* [`unified-message-control`](https://github.com/unifiedjs/unified-message-control) - — Enable, disable, and ignore messages +Around each syntax tree is an ecosystem that focusses on that particular kind +of content. +At their core, they parse text to a tree and compile that tree back to text. +They also provide plugins that work with the syntax tree, without requiring +that the end user has knowledge about that tree. -See [**remark**][remark-plugins], [**rehype**][rehype-plugins], and -[**retext**][retext-plugins] for their lists of plugins. +* [rehype][] (hast) — HTML +* [remark][] (mdast) — markdown +* [retext][] (nlcst) — natural language -###### File + + +###### Plugins -When processing a document, **metadata** is often gathered about that document. -[**vfile**][vfile] is a virtual file format that stores data, metadata, and -messages about files for **unified** and its plugins. +Each aforementioned ecosystem comes with a large set of plugins that you can +pick and choose from to do all kinds of things. -There are several [*utilities*][vfile-utilities] for working with these files. +* [List of remark plugins][remark-plugins] · + [`remarkjs/awesome-remark`][awesome-remark] · + [`remark-plugin` topic][topic-remark-plugin] +* [List of rehype plugins][rehype-plugins] · + [`rehypejs/awesome-rehype`][awesome-rehype] · + [`rehype-plugin` topic][topic-rehype-plugin] +* [List of retext plugins][retext-plugins] · + [`retextjs/awesome-retext`][awesome-retext] · + [`retext-plugin` topic][topic-retext-plugin] + +There are also a few plugins that work in any ecosystem: + +* [`unified-diff`](https://github.com/unifiedjs/unified-diff) + — ignore unrelated messages in GitHub Actions and Travis +* [`unified-infer-git-meta`](https://github.com/unifiedjs/unified-infer-git-meta) + — infer metadata of a document from Git +* [`unified-message-control`](https://github.com/unifiedjs/unified-message-control) + — enable, disable, and ignore messages from content ###### Configuration -[*Processors*][processors] are configured with [*plugin*][plugin]s or -with the [`data`][data] method. +Processors are configured with [plugins][plugin] or with the [`data`][data] +method. +Most plugins also accept configuration through options. +See each plugin’s readme for more info. ###### Integrations -**unified** can integrate with the file system with [`unified-engine`][engine]. -CLI apps can be created with [`unified-args`][args], Gulp plugins with -[`unified-engine-gulp`][gulp]. - -[`unified-stream`][stream] provides a streaming interface. +unified can integrate with the file system through +[`unified-engine`][unified-engine]. +CLI apps can be created with [`unified-args`][unified-args], Gulp plugins with +[`unified-engine-gulp`][unified-engine-gulp], and language servers with +[`unified-language-server`][unified-language-server]. +A streaming interface can be created with [`unified-stream`][unified-stream]. ###### Programming interface -The API provided by **unified** allows multiple files to be processed and gives -access to *metadata* (such as lint messages): +The [API][] provided by `unified` allows multiple files to be processed and +gives access to metadata (such as lint messages): ```js import {unified} from 'unified' @@ -299,23 +309,16 @@ import remarkRehype from 'remark-rehype' import rehypeStringify from 'rehype-stringify' import {reporter} from 'vfile-reporter' -unified() +const file = await unified() .use(remarkParse) .use(remarkPresetLintMarkdownStyleGuide) .use(remarkRetext, unified().use(retextEnglish).use(retextEquality)) .use(remarkRehype) .use(rehypeStringify) .process('*Emphasis* and _stress_, you guys!') - .then( - (file) => { - console.error(reporter(file)) - console.log(String(file)) - }, - (error) => { - // Handle your error here! - throw error - } - ) + +console.error(reporter(file)) +console.log(String(file)) ``` Yields: @@ -331,68 +334,78 @@ Yields:

Emphasis and stress, you guys!

``` -###### Processing between syntaxes + + + -[*Processors*][processors] can be combined in two modes. +###### Transforming between ecosystems -**Bridge** mode transforms the [*syntax tree*][syntax-tree] from one format -(*origin*) to another (*destination*). -Another processor runs on the destination tree. -Finally, the original processor continues transforming the origin tree. +Ecosystems can be combined in two modes. + +**Bridge** mode transforms the tree from one format (*origin*) to another +(*destination*). +A different processor runs on the destination tree. +Afterwards, the original processor continues with the origin tree. **Mutate** mode also transforms the syntax tree from one format to another. But the original processor continues transforming the destination tree. In the previous example (“Programming interface”), `remark-retext` is used in -*bridge* mode: the origin syntax tree is kept after [**retext**][retext] is -done; whereas `remark-rehype` is used in *mutate* mode: it sets a new syntax -tree and discards the origin tree. +bridge mode: the origin syntax tree is kept after retext is done; whereas +`remark-rehype` is used in mutate mode: it sets a new syntax tree and discards +the origin tree. + +The following plugins lets you combine ecosystems: -* [`remark-retext`][remark-retext] -* [`remark-rehype`][remark-rehype] -* [`rehype-retext`][rehype-retext] -* [`rehype-remark`][rehype-remark] +* [`remark-retext`][remark-retext] — turn markdown into natural language +* [`remark-rehype`][remark-rehype] — turn markdown into HTML +* [`rehype-retext`][rehype-retext] — turn HTML into natural language +* [`rehype-remark`][rehype-remark] — turn HTML into markdown ## API -This package exports the following identifiers: `unified`. +This package exports the identifier `unified` (the root `processor`). There is no default export. ### `processor()` -[*Processor*][processors] describing how to *process* text. +Create a processor. ###### Returns -`Function` — New [*unfrozen*][freeze] processor that is configured to work the +New *[unfrozen][freeze]* processor (`processor`) that is configured to work the same as its ancestor. When the descendant processor is configured in the future it does not affect the ancestral processor. ###### Example -The following example shows how a new processor can be created (from the remark -processor) and linked to **stdin**(4) and **stdout**(4). +This example shows how a new processor can be created (from `remark`) and linked +to **stdin**(4) and **stdout**(4). ```js -import {remark} from 'remark' +import process from 'node:process' import concatStream from 'concat-stream' +import {remark} from 'remark' process.stdin.pipe( concatStream((buf) => { - process.stdout.write(remark().processSync(buf).toString()) + process.stdout.write(String(remark().processSync(buf))) }) ) ``` ### `processor.use(plugin[, options])` -[*Configure*][configuration] the processor to use a [*plugin*][plugin] and -optionally configure that plugin with options. +Configure the processor to use a plugin and optionally configure that plugin +with options. -If the processor is already using this plugin, the previous plugin configuration +If the processor is already using a plugin, the previous plugin configuration is changed based on the options that are passed in. -The plugin is not added a second time. +In other words, the plugin is not added a second time. + +> 👉 **Note**: `use` cannot be called on *[frozen][freeze]* processors. +> Call the processor first to create a new unfrozen processor. ###### Signatures @@ -403,25 +416,20 @@ The plugin is not added a second time. ###### Parameters * `plugin` ([`Attacher`][plugin]) -* `options` (`*`, optional) — Configuration for `plugin` -* `preset` (`Object`) — Object with an optional `plugins` (set to `list`), +* `options` (`*`, optional) — configuration for `plugin` +* `preset` (`Object`) — object with an optional `plugins` (set to `list`), and/or an optional `settings` object -* `list` (`Array`) — List of plugins, presets, and pairs (`plugin` and +* `list` (`Array`) — list of plugins, presets, and pairs (`plugin` and `options` in an array) ###### Returns -`processor` — The processor that `use` was called on. - -###### Note - -`use` cannot be called on [*frozen*][freeze] processors. -Call the processor first to create a new unfrozen processor. +The processor that `use` was called on (`processor`). ###### Example There are many ways to pass plugins to `.use()`. -The below example gives an overview. +This example gives an overview: ```js import {unified} from 'unified' @@ -443,27 +451,24 @@ unified() ### `processor.parse(file)` -Parse text to a [*syntax tree*][syntax-tree]. +Parse text to a syntax tree. -###### Parameters +> 👉 **Note**: `parse` freezes the processor if not already *[frozen][freeze]*. -* `file` ([`VFile`][vfile]) — [*File*][file], any value accepted by `vfile()` - -###### Returns +> 👉 **Note**: `parse` performs the [parse phase][overview], not the run phase +> or other phases. -[`Node`][node] — Parsed [*syntax tree*][syntax-tree] representing `file`. +###### Parameters -###### Note +* `file` ([`VFile`][vfile]) — any value accepted as `x` in `new VFile(x)` -`parse` freezes the processor if not already [*frozen*][freeze]. +###### Returns -`parse` performs the [*parse phase*][description], not the *run phase* or other -phases. +Syntax tree representing `file` ([`Node`][node]). ###### Example -The below example shows how `parse` can be used to create a syntax tree from a -file. +This example shows how `parse` can be used to create a tree from a file. ```js import {unified} from 'unified' @@ -491,50 +496,49 @@ Yields: #### `processor.Parser` -A **parser** handles the parsing of text to a [*syntax tree*][syntax-tree]. -Used in the [*parse phase*][description] and called with a `string` and -[`VFile`][vfile] representation of the text to parse. +A **parser** handles the parsing of text to a syntax tree. +It is used in the [parse phase][overview] and is called with a `string` and +[`VFile`][vfile] of the document to parse. -`Parser` can be a function, in which case it must return a [`Node`][node]: the -syntax tree representation of the given file. +`Parser` can be a normal function, in which case it must return the syntax +tree representation of the given file ([`Node`][node]). `Parser` can also be a constructor function (a function with a `parse` field, or -other fields, in its `prototype`), in which case it’s constructed with `new`. +other fields, in its `prototype`), in which case it is constructed with `new`. Instances must have a `parse` method that is called without arguments and must return a [`Node`][node]. -### `processor.stringify(node[, file])` +### `processor.stringify(tree[, file])` -Compile a [*syntax tree*][syntax-tree]. +Compile a syntax tree. -###### Parameters +> 👉 **Note**: `stringify` freezes the processor if not already +> *[frozen][freeze]*. -* `node` ([`Node`][node]) — [*Syntax tree*][syntax-tree] to compile -* `file` ([`VFile`][vfile], optional) — [*File*][file], any value accepted by - `vfile()` - -###### Returns +> 👉 **Note**: `stringify` performs the [stringify phase][overview], not the run +> phase or other phases. -`string` or `Buffer` (see notes) — Textual representation of the [*syntax -tree*][syntax-tree] +###### Parameters -###### Note +* `tree` ([`Node`][node]) — tree to compile +* `file` ([`VFile`][vfile], optional) — any value accepted as `x` in + `new VFile(x)` -`stringify` freezes the processor if not already [*frozen*][freeze]. +###### Returns -`stringify` performs the [*stringify phase*][description], not the *run phase* -or other phases. +Textual representation of the tree (`string` or `Buffer`, see note). -unified typically compiles by serializing: most [*compiler*][compiler]s return -`string` (or `Buffer`). -Some compilers, such as the one configured with [`rehype-react`][rehype-react], -return other values (in this case, a React tree). -If you’re using a compiler doesn’t serialize, expect different result values. -When using TypeScript, cast the type on your side. +> 👉 **Note**: unified typically compiles by serializing: most +> [compilers][compiler] return `string` (or `Buffer`). +> Some compilers, such as the one configured with +> [`rehype-react`][rehype-react], return other values (in this case, a React +> tree). +> If you’re using a compiler that doesn’t serialize, expect different result +> values. ###### Example -The below example shows how `stringify` can be used to serialize a syntax tree. +This example shows how `stringify` can be used to serialize a syntax tree: ```js import {unified} from 'unified' @@ -556,56 +560,53 @@ Yields: #### `processor.Compiler` -A **compiler** handles the compiling of a [*syntax tree*][syntax-tree] to text. -Used in the [*stringify phase*][description] and called with a [`Node`][node] -and [`VFile`][file] representation of syntax tree to compile. +A **compiler** handles the compiling of a syntax tree to something else (in +most cases, text). +It is used in the [stringify phase][overview] and called with a [`Node`][node] +and [`VFile`][file] representation of the document to compile. -`Compiler` can be a function, in which case it should return a `string`: the -textual representation of the syntax tree. +`Compiler` can be a normal function, in which case it should return the textual +representation of the given tree (`string`). `Compiler` can also be a constructor function (a function with a `compile` -field, or other fields, in its `prototype`), in which case it’s constructed with -`new`. +field, or other fields, in its `prototype`), in which case it is constructed +with `new`. Instances must have a `compile` method that is called without arguments and should return a `string`. -### `processor.run(node[, file][, done])` - -Run [*transformers*][transformer] on a [*syntax tree*][syntax-tree]. - -###### Parameters - -* `node` ([`Node`][node]) — [*Syntax tree*][syntax-tree] to run on -* `file` ([`VFile`][vfile], optional) — [*File*][file], any value accepted by - `vfile()` -* `done` ([`Function`][run-done], optional) — Callback - -###### Returns +> 👉 **Note**: unified typically compiles by serializing: most compilers +> return `string` (or `Buffer`). +> Some compilers, such as the one configured with +> [`rehype-react`][rehype-react], return other values (in this case, a React +> tree). +> If you’re using a compiler that doesn’t serialize, expect different result +> values. -[`Promise`][promise] if `done` is not given. -The returned promise is rejected with a fatal error, or resolved with the -transformed [*syntax tree*][syntax-tree]. +### `processor.run(tree[, file][, done])` -###### Note +Run *[transformers][transformer]* on a syntax tree. -`run` freezes the processor if not already [*frozen*][freeze]. +> 👉 **Note**: `run` freezes the processor if not already *[frozen][freeze]*. -`run` performs the [*run phase*][description], not other phases. +> 👉 **Note**: `run` performs the [run phase][overview], not other phases. -#### `function done(err[, node, file])` +###### Parameters -Callback called when [*transformers*][transformer] are done. -Called with either an error or results. +* `tree` ([`Node`][node]) — tree to transform and inspect +* `file` ([`VFile`][vfile], optional) — any value accepted as `x` in + `new VFile(x)` +* `done` ([`Function`][run-done], optional) — callback -###### Parameters +###### Returns -* `err` (`Error`, optional) — Fatal error -* `node` ([`Node`][node], optional) — Transformed [*syntax tree*][syntax-tree] -* `file` ([`VFile`][vfile], optional) — [*File*][file] +Nothing if `done` is given (`void`). +A [`Promise`][promise] otherwise. +The promise is rejected with a fatal error or resolved with the transformed +tree ([`Node`][node]). ###### Example -The below example shows how `run` can be used to transform a syntax tree. +This example shows how `run` can be used to transform a tree: ```js import {unified} from 'unified' @@ -618,16 +619,9 @@ const tree = u('root', [ ]) ]) -unified() - .use(remarkReferenceLinks) - .run(tree) - .then( - (changedTree) => console.log(changedTree), - (error) => { - // Handle your error here! - throw error - } - ) +const changedTree = await unified().use(remarkReferenceLinks).run(tree) + +console.log(changedTree) ``` Yields: @@ -642,65 +636,73 @@ Yields: } ``` -### `processor.runSync(node[, file])` - -Run [*transformers*][transformer] on a [*syntax tree*][syntax-tree]. +#### `function done(err[, tree, file])` -An error is thrown if asynchronous [*plugin*][plugin]s are configured. +Callback called when transformers are done. +Called with either an error or results. ###### Parameters -* `node` ([`Node`][node]) — [*Syntax tree*][syntax-tree] to run on -* `file` ([`VFile`][vfile], optional) — [*File*][file], any value accepted by - `vfile()` +* `err` (`Error`, optional) — fatal error +* `tree` ([`Node`][node], optional) — transformed tree +* `file` ([`VFile`][vfile], optional) — file -###### Returns +### `processor.runSync(tree[, file])` -[`Node`][node] — Transformed [*syntax tree*][syntax-tree]. +Run *[transformers][transformer]* on a syntax tree. +An error is thrown if asynchronous transforms are configured. -###### Note +> 👉 **Note**: `runSync` freezes the processor if not already +> *[frozen][freeze]*. -`runSync` freezes the processor if not already [*frozen*][freeze]. +> 👉 **Note**: `runSync` performs the [run phase][overview], not other phases. -`runSync` performs the [*run phase*][description], not other phases. +###### Parameters -### `processor.process(file[, done])` +* `tree` ([`Node`][node]) — tree to transform and inspect +* `file` ([`VFile`][vfile], optional) — any value accepted as `x` in + `new VFile(x)` -[*Process*][description] the given [*file*][file] as configured on the -processor. +###### Returns -###### Parameters +Transformed tree ([`Node`][node]). -* `file` ([`VFile`][vfile]) — [*File*][file], any value accepted by `vfile()` -* `done` ([`Function`][process-done], optional) — Callback +### `processor.process(file[, done])` -###### Returns +Process the given file as configured on the processor. -[`Promise`][promise] if `done` is not given. -The returned promise is rejected with a fatal error, or resolved with the -processed [*file*][file]. +> 👉 **Note**: `process` freezes the processor if not already +> *[frozen][freeze]*. -The parsed, transformed, and compiled value is exposed on -[`file.value`][vfile-value] or `file.result` (see notes). +> 👉 **Note**: `process` performs the [parse, run, and stringify +> phases][overview]. -###### Note +###### Parameters -`process` freezes the processor if not already [*frozen*][freeze]. +* `file` ([`VFile`][vfile]) — any value accepted as `x` in `new VFile(x)` +* `done` ([`Function`][process-done], optional) — callback -`process` performs the [*parse*, *run*, and *stringify* phases][description]. +###### Returns -unified typically compiles by serializing: most [*compiler*][compiler]s return -`string` (or `Buffer`). -Some compilers, such as the one configured with [`rehype-react`][rehype-react], -return other values (in this case, a React tree). -If you’re using a compiler that serializes, the result is available at -`file.value`. -Otherwise, the result is available at `file.result`. +Nothing if `done` is given (`void`). +A [`Promise`][promise] otherwise. +The promise is rejected with a fatal error or resolved with the processed +file ([`VFile`][vfile]). + +The parsed, transformed, and compiled value is available at +[`file.value`][vfile-value] (see note). + +> 👉 **Note**: unified typically compiles by serializing: most +> [compilers][compiler] return `string` (or `Buffer`). +> Some compilers, such as the one configured with +> [`rehype-react`][rehype-react], result in other values (in this case, a React +> tree). +> If you’re using a compiler that does not serialize, the result is available +> at `file.result`. ###### Example -The below example shows how `process` can be used to process a file, whether -transformers are asynchronous or not, with promises. +This example shows how `process` can be used to process a file: ```js import {unified} from 'unified' @@ -710,20 +712,15 @@ import rehypeDocument from 'rehype-document' import rehypeFormat from 'rehype-format' import rehypeStringify from 'rehype-stringify' -unified() +const file = await unified() .use(remarkParse) .use(remarkRehype) .use(rehypeDocument, {title: '👋🌍'}) .use(rehypeFormat) .use(rehypeStringify) .process('# Hello world!') - .then( - (file) => console.log(String(file)), - (error) => { - // Handle your error here! - throw error - } - ) + +console.log(String(file)) ``` Yields: @@ -744,18 +741,17 @@ Yields: #### `function done(err, file)` -Callback called when the [*process*][description] is done. -Called with a fatal error, if any, and a [*file*][file]. +Callback called when the process is done. +Called with either an error or a result. ###### Parameters -* `err` (`Error`, optional) — Fatal error -* `file` ([`VFile`][vfile]) — Processed [*file*][file] +* `err` (`Error`, optional) — fatal error +* `file` ([`VFile`][vfile]) — processed file ###### Example -The below example shows how `process` can be used to process a file, whether -transformers are asynchronous or not, with a callback. +This example shows how `process` can be used to process a file with a callback. ```js import {unified} from 'unified' @@ -768,17 +764,12 @@ unified() .use(remarkParse) .use(remarkGithub) .use(remarkStringify) - .process('@unifiedjs') - .then( - (file) => { - console.error(reporter(file)) + .process('@unifiedjs', function (error, file) { + console.error(reporter(error || file)) + if (file) { console.log(String(file)) - }, - (error) => { - // Handle your error here! - throw error } - ) + }) ``` Yields: @@ -791,42 +782,39 @@ no issues found [**@unifiedjs**](https://github.com/unifiedjs) ``` -### `processor.processSync(file|value)` +### `processor.processSync(file)` + +Process the given file as configured on the processor. +An error is thrown if asynchronous transforms are configured. -[*Process*][description] the given [*file*][file] as configured on the -processor. +> 👉 **Note**: `processSync` freezes the processor if not already +> *[frozen][freeze]*. -An error is thrown if asynchronous [*plugin*][plugin]s are configured. +> 👉 **Note**: `processSync` performs the [parse, run, and stringify +> phases][overview]. ###### Parameters -* `file` ([`VFile`][vfile]) — [*File*][file], any value accepted by `vfile()` +* `file` ([`VFile`][vfile]) — any value accepted as `x` in `new VFile(x)` ###### Returns -([`VFile`][vfile]) — Processed [*file*][file] - -The parsed, transformed, and compiled value is exposed on -[`file.value`][vfile-value] or `file.result` (see notes). - -###### Note - -`processSync` freezes the processor if not already [*frozen*][freeze]. +The processed file ([`VFile`][vfile]). -`processSync` performs the [*parse*, *run*, and *stringify* -phases][description]. +The parsed, transformed, and compiled value is available at +[`file.value`][vfile-value] (see note). -unified typically compiles by serializing: most [*compiler*][compiler]s return -`string` (or `Buffer`). -Some compilers, such as the one configured with [`rehype-react`][rehype-react], -return other values (in this case, a React tree). -If you’re using a compiler that serializes, the result is available at -`file.value`. -Otherwise, the result is available at `file.result`. +> 👉 **Note**: unified typically compiles by serializing: most +> [compilers][compiler] return `string` (or `Buffer`). +> Some compilers, such as the one configured with +> [`rehype-react`][rehype-react], result in other values (in this case, a React +> tree). +> If you’re using a compiler that does not serialize, the result is available +> at `file.result`. ###### Example -The below example shows how `processSync` can be used to process a file, if all +This example shows how `processSync` can be used to process a file, if all transformers are synchronous. ```js @@ -844,7 +832,7 @@ const processor = unified() .use(rehypeFormat) .use(rehypeStringify) -console.log(processor.processSync('# Hello world!').toString()) +console.log(String(processor.processSync('# Hello world!'))) ``` Yields: @@ -865,14 +853,17 @@ Yields: ### `processor.data([key[, value]])` -[*Configure*][configuration] the processor with information available to all -[*plugin*][plugin]s. -Information is stored in an in-memory key-value store. +Configure the processor with info available to all plugins. +Information is stored in an object. Typically, options can be given to a specific plugin, but sometimes it makes sense to have information shared with several plugins. For example, a list of HTML elements that are self-closing, which is needed -during all [*phases*][description] of the *process*. +during all [phases][overview]. + +> 👉 **Note**: setting information cannot occur on *[frozen][freeze]* +> processors. +> Call the processor first to create a new unfrozen processor. ###### Signatures @@ -883,24 +874,19 @@ during all [*phases*][description] of the *process*. ###### Parameters -* `key` (`string`, optional) — Identifier -* `value` (`*`, optional) — Value to set -* `values` (`Object`, optional) — Values to set +* `key` (`string`, optional) — identifier +* `value` (`*`, optional) — value to set +* `values` (`Object`, optional) — values to set ###### Returns -* `processor` — If setting, the processor that `data` is called on -* `value` (`*`) — If getting, the value at `key` -* `info` (`Object`) — Without arguments, the key-value store - -###### Note - -Setting information cannot occur on [*frozen*][freeze] processors. -Call the processor first to create a new unfrozen processor. +* `processor` — when setting, the processor that `data` is called on +* `value` (`*`) — when getting, the value at `key` +* `info` (`Object`) — without arguments, the key-value store ###### Example -The following example show how to get and set information: +This example show how to get and set info: ```js import {unified} from 'unified' @@ -918,35 +904,34 @@ processor.data() // => {charlie: 'delta'} ### `processor.freeze()` -**Freeze** a processor. -*Frozen* processors are meant to be extended and not to be configured directly. +Freeze a processor. +Frozen processors are meant to be extended and not to be configured directly. -Once a processor is frozen it cannot be *unfrozen*. +When a processor is frozen it cannot be unfrozen. New processors working the same way can be created by calling the processor. It’s possible to freeze processors explicitly by calling `.freeze()`. -Processors freeze implicitly when [`.parse()`][parse], [`.run()`][run], +Processors freeze automatically when [`.parse()`][parse], [`.run()`][run], [`.runSync()`][run-sync], [`.stringify()`][stringify], [`.process()`][process], or [`.processSync()`][process-sync] are called. ###### Returns -`processor` — The processor that `freeze` was called on. +The processor that `freeze` was called on (`processor`). ###### Example -The following example, `index.js`, shows how rehype prevents extensions to -itself: +This example, `index.js`, shows how `rehype` prevents extensions to itself: ```js import {unified} from 'unified' -import remarkParse from 'rehype-parse' -import remarkStringify from 'rehype-stringify' +import rehypeParse from 'rehype-parse' +import rehypeStringify from 'rehype-stringify' -export const rehype = unified().use(remarkParse).use(remarkStringify).freeze() +export const rehype = unified().use(rehypeParse).use(rehypeStringify).freeze() ``` -The below example, `a.js`, shows how that processor can be used and configured. +That processor can be used and configured like so: ```js import {rehype} from 'rehype' @@ -958,11 +943,10 @@ rehype() // … ``` -The below example, `b.js`, shows a similar looking example that operates on the -frozen rehype interface because it does not call `rehype`. -If this behavior was allowed it would result in unexpected behavior so an -error is thrown. -**This is invalid**: +A similar looking example is broken as operates on the frozen interface. +If this behavior was allowed it would result in unexpected behavior so an error +is thrown. +**This is not valid**: ```js import {rehype} from 'rehype' @@ -985,38 +969,41 @@ Error: Cannot call `use` on a frozen processor. Create a new processor first, by calling it: use `processor()` instead of `processor`. at assertUnfrozen (~/node_modules/unified/index.js:426:11) at Function.use (~/node_modules/unified/index.js:165:5) - at ~/b.js:6:4 + … ``` ## `Plugin` -**Plugins** [*configure*][configuration] the processors they are applied on in -the following ways: +**Plugins** configure the processors they are applied on in the following ways: -* They change the processor: such as the [*parser*][parser], the - [*compiler*][compiler], or configuring [*data*][data] -* They specify how to handle [*syntax trees*][syntax-tree] and [*files*][file] +* they change the processor, such as the [parser][], the [compiler][], or by + configuring [data][] +* they specify how to handle trees and files Plugins are a concept. -They materialize as [`attacher`][attacher]s. +They materialize as [`Attacher`s][attacher]. ###### Example `move.js`: ```js -export function move(options = {}) { - const {extname} = options - - if (!extname) { - throw new Error('Missing `extname` in options') +/** + * @typedef Options + * Configuration (required). + * @property {string} extname + * File extension to use (must start with `.`). + */ + +/** @type {import('unified').Plugin<[Options]>} */ +export function move(options) { + if (!options || !options.extname) { + throw new Error('Missing `options.extname`') } - return transformer - - function transformer(tree, file) { - if (file.extname && file.extname !== extname) { - file.extname = extname + return function (tree, file) { + if (file.extname && file.extname !== options.extname) { + file.extname = options.extname } } } @@ -1031,30 +1018,24 @@ export function move(options = {}) { `index.js`: ```js +import {read, write} from 'to-vfile' +import {reporter} from 'vfile-reporter' import {unified} from 'unified' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import rehypeStringify from 'rehype-stringify' -import {toVFile} from 'to-vfile' -import {reporter} from 'vfile-reporter' import {move} from './move.js' -unified() +const file = await unified() .use(remarkParse) .use(remarkRehype) .use(move, {extname: '.html'}) .use(rehypeStringify) - .process(toVFile.readSync('index.md')) - .then( - (file) => { - console.error(reporter(file)) - toVFile.writeSync(file) // Written to `index.html`. - }, - (error) => { - // Handle your error here! - throw error - } - ) + .process(await read('index.md')) + +console.error(reporter(file)) +await write(file) // Written to `index.html`. + ``` Yields: @@ -1069,78 +1050,66 @@ index.md: no issues found

Hello, world!

``` -### `function attacher([options])` - -**Attachers** are materialized [*plugin*][plugin]s. -An attacher is a function that can receive options and -[*configures*][configuration] the processor. +### `function attacher(options?)` -Attachers change the processor, such as the [*parser*][parser], the -[*compiler*][compiler], configuring [*data*][data], or by specifying how the -[*syntax tree*][syntax-tree] or [*file*][file] are handled. +Attachers are materialized plugins. +They are functions that can receive options and configure the processor. -###### Context +Attachers change the processor, such as the [parser][], the [compiler][], +by configuring [data][], or by specifying how the tree and file are handled. -The context object (`this`) is set to the processor the attacher is applied on. +> 👉 **Note**: attachers are called when the processor is *[frozen][freeze]*, +> not when they are applied. ###### Parameters -* `options` (`*`, optional) — Configuration +* `this` (`processor`) — processor the attacher is applied to +* `options` (`*`, optional) — configuration ###### Returns -[`transformer`][transformer] — Optional. - -###### Note - -Attachers are called when the processor is [*frozen*][freeze], not when they are -applied. +Optional transform ([`Transformer`][transformer]). -### `function transformer(node, file[, next])` +### `function transformer(tree, file[, next])` -**Transformers** handle [*syntax tree*][syntax-tree]s and [*file*][file]s. -A transformer is a function that is called each time a syntax tree and file are -passed through the [*run phase*][description]. -If an error occurs (either because it’s thrown, returned, rejected, or passed to -[`next`][next]), the process stops. +Transformers handle syntax trees and files. +They are functions that are called each time a syntax tree and file are passed +through the [run phase][overview]. +When an error occurs in them (either because it’s thrown, returned, rejected, +or passed to [`next`][next]), the process stops. -The *run phase* is handled by [`trough`][trough], see its documentation for the +The run phase is handled by [`trough`][trough], see its documentation for the exact semantics of these functions. ###### Parameters -* `node` ([`Node`][node]) — [*Syntax tree*][syntax-tree] to handle -* `file` ([`VFile`][vfile]) — [*File*][file] to handle +* `tree` ([`Node`][node]) — tree to handle +* `file` ([`VFile`][vfile]) —file to handle * `next` ([`Function`][next], optional) ###### Returns -* `void` — If nothing is returned, the next transformer keeps using same tree. -* `Error` — Fatal error to stop the process -* `node` ([`Node`][node]) — New [*syntax tree*][syntax-tree]. - If returned, the next transformer is given this new tree -* `Promise` — Returned to perform an asynchronous operation. - The promise **must** be resolved (optionally with a [`Node`][node]) or - rejected (optionally with an `Error`) +* `void` — the next transformer keeps using same tree +* `Error` — fatal error to stop the process +* [`Node`][node] — new, changed, tree +* `Promise` — resolved with a new, changed, tree or rejected with an + `Error` #### `function next(err[, tree[, file]])` -If the signature of a [*transformer*][transformer] includes `next` (the third -argument), the transformer **may** perform asynchronous operations, and **must** -call `next()`. +If the signature of a `transformer` accepts a third argument, the transformer +may perform asynchronous operations, and must call `next()`. ###### Parameters -* `err` (`Error`, optional) — Fatal error to stop the process -* `node` ([`Node`][node], optional) — New [*syntax tree*][syntax-tree]. - If given, the next transformer is given this new tree -* `file` ([`VFile`][vfile], optional) — New [*file*][file]. - If given, the next transformer is given this new file +* `err` (`Error`, optional) — fatal error to stop the process +* `tree` ([`Node`][node], optional) — new, changed, tree +* `file` ([`VFile`][vfile], optional) — new, changed, file ## `Preset` -**Presets** are sharable [*configuration*][configuration]. -They can contain [*plugins*][plugin] and settings. +Presets are sharable configuration. +They can contain plugins and settings. ###### Example @@ -1182,24 +1151,17 @@ _Emphasis_ and **importance**. `index.js`: ```js +import {read, write} from 'to-vfile' import {remark} from 'remark' -import {toVFile} from 'to-vfile' import {reporter} from 'vfile-reporter' import {preset} from './preset.js' -remark() +const file = await remark() .use(preset) - .process(toVFile.readSync('example.md')) - .then( - (file) => { - console.error(reporter(file)) - toVFile.writeSync(file) - }, - (error) => { - // Handle your error here! - throw error - } - ) + .process(await read('example.md')) + +console.error(reporter(file)) +await write(file) ``` Yields: @@ -1227,31 +1189,203 @@ example.md: no issues found [MIT](license) © [Titus Wormer](https://wooorm.com) ``` +## Types + +This package is fully typed with [TypeScript][]. +It exports the following additional types: + +* `Processor` + — processor, where `ParseTree` is the tree that the parser creates, + `CurrentTree` the tree that the current plugin yields, `CompileTree` the + tree that the compiler accepts, and `CompileResult` the thing that the + compiler yields +* `FrozenProcessor` + — like `Processor` but frozen +* `Plugin` + — plugin, where `PluginParameters` are the accepted arguments, `Input` the + input value, and `Output` the output value (see below) +* `Pluggable` +* `Preset` + — preset +* `PluginTuple` + — plugin tuple +* `Pluggable` + — any usable value, where `PluginParameters` are the accepted arguments +* `PluggableList` + — list of plugins and presets +* `Transformer` + — transformer, where `Input` and `Output` are the input/output trees +* `TransformCallback` + — third argument of a transformer +* `Parser` + — parser as a class or normal function, where `Tree` is the resulting tree +* `ParserClass` + — parser class +* `ParserFunction` + — parser function +* `Compiler` + — compiler as a class or normal function, where `Tree` is the accepted tree + and `Result` the thing that the compiler yields +* `CompilerClass` + — compiler class +* `CompilerFunction` + — compiler function +* `RunCallback` + — callback to `run`, where `Tree` is the resulting tree +* `ProcessCallback` + — callback to `process`, where `File` is the resulting file + +For TypeScript to work, it is particularly important to type your plugins +correctly. +We strongly recommend using the `Plugin` type with its generics and to use the +node types for the syntax trees provided by our packages (as in, +[`@types/hast`][types-hast], [`@types/mdast`][types-mdast], +[`@types/nlcst`][types-nlcst]). + +```js +/** + * @typedef {import('mdast').Root} MdastRoot + * @typedef {import('hast').Root} HastRoot + * + * @typedef Options + * Configuration (optional). + * @property {boolean} [someField] + * Some option. + */ + +// To type options: +/** @type {import('unified').Plugin<[Options?]>} */ +export function myPluginAcceptingOptions(options) { + // `options` is `Options?`. +} + +// To type a plugin that works on a certain tree: +/** @type {import('unified').Plugin<[], MdastRoot>} */ +export function myRemarkPlugin() { + return function (tree, file) { + // `tree` is `MdastRoot`. + } +} + +// To type a plugin that transforms one tree into another: +/** @type {import('unified').Plugin<[], MdastRoot, HastRoot>} */ +export function remarkRehype() { + return function (tree) { + // `tree` is `MdastRoot`. + // Result must be `HastRoot`. + } +} + +// To type a plugin that defines a parser: +/** @type {import('unified').Plugin<[], string, MdastRoot>} */ +export function remarkParse(options) {} + +// To type a plugin that defines a compiler: +/** @type {import('unified').Plugin<[], HastRoot, string>} */ +export function rehypeStringify(options) {} +``` + +## Compatibility + +Projects maintained by the unified collective are compatible with all maintained +versions of Node.js. +As of now, that is Node.js 12.20+, 14.14+, 16.0+, and 18.0+. +Our projects sometimes work with older versions, but this is not guaranteed. + ## Contribute See [`contributing.md`][contributing] in [`unifiedjs/.github`][health] for ways to get started. See [`support.md`][support] for ways to get help. -Ideas for new plugins and tools can be posted in [`unifiedjs/ideas`][ideas]. - -A curated list of awesome unified resources can be found in [**awesome -unified**][awesome]. This project has a [code of conduct][coc]. By interacting with this repository, organization, or community you agree to abide by its terms. +For info on how to submit a security report, see our +[security policy][security]. + +## Sponsor + +Support this effort and give back by sponsoring on [OpenCollective][collective]! + + + + + + + + + + + + + + + + + + + + + + + + +
+ Vercel

+ +
+ Motif

+ +
+ HashiCorp

+ +
+ American Express

+ +
+ GitBook

+ +
+ Gatsby

+ +
+ Netlify

+ + +
+ Coinbase

+ +
+ ThemeIsle

+ +
+ Expo

+ +
+ Boost Note

+ +
+ Holloway

+ +
+
+ You? +

+
+ ## Acknowledgments Preliminary work for unified was done [in 2014][preliminary] for -[**retext**][retext] and inspired by [`ware`][ware]. -Further incubation happened in [**remark**][remark]. +**[retext][]** and inspired by [`ware`][ware]. +Further incubation happened in **[remark][]**. The project was finally [externalised][] in 2015 and [published][] as `unified`. -The project was authored by [**@wooorm**](https://github.com/wooorm). +The project was authored by **[@wooorm](https://github.com/wooorm)**. Although `unified` since moved its plugin architecture to [`trough`][trough], -thanks to [**@calvinfo**](https://github.com/calvinfo), -[**@ianstormtaylor**](https://github.com/ianstormtaylor), and others for their +thanks to **[@calvinfo](https://github.com/calvinfo)**, +**[@ianstormtaylor](https://github.com/ianstormtaylor)**, and others for their work on [`ware`][ware], as it was a huge initial inspiration. ## License @@ -1288,15 +1422,21 @@ work on [`ware`][ware], as it was a huge initial inspiration. [chat]: https://github.com/unifiedjs/unified/discussions +[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c + +[esmsh]: https://esm.sh + +[typescript]: https://www.typescriptlang.org + [health]: https://github.com/unifiedjs/.github -[contributing]: https://github.com/unifiedjs/.github/blob/HEAD/contributing.md +[contributing]: https://github.com/unifiedjs/.github/blob/main/contributing.md -[support]: https://github.com/unifiedjs/.github/blob/HEAD/support.md +[support]: https://github.com/unifiedjs/.github/blob/main/support.md -[coc]: https://github.com/unifiedjs/.github/blob/HEAD/code-of-conduct.md +[coc]: https://github.com/unifiedjs/.github/blob/main/code-of-conduct.md -[awesome]: https://github.com/unifiedjs/awesome-unified +[security]: https://github.com/unifiedjs/.github/blob/main/security.md [license]: license @@ -1308,14 +1448,14 @@ work on [`ware`][ware], as it was a huge initial inspiration. [twitter]: https://twitter.com/unifiedjs -[learn]: https://unifiedjs.com/learn/ - [rehype]: https://github.com/rehypejs/rehype [remark]: https://github.com/remarkjs/remark [retext]: https://github.com/retextjs/retext +[syntax-tree]: https://github.com/syntax-tree + [esast]: https://github.com/syntax-tree/esast [hast]: https://github.com/syntax-tree/hast @@ -1328,11 +1468,15 @@ work on [`ware`][ware], as it was a huge initial inspiration. [unist]: https://github.com/syntax-tree/unist -[engine]: https://github.com/unifiedjs/unified-engine +[unified-engine]: https://github.com/unifiedjs/unified-engine + +[unified-args]: https://github.com/unifiedjs/unified-args + +[unified-engine-gulp]: https://github.com/unifiedjs/unified-engine-gulp -[args]: https://github.com/unifiedjs/unified-args +[unified-language-server]: https://github.com/unifiedjs/unified-language-server -[gulp]: https://github.com/unifiedjs/unified-engine-gulp +[unified-stream]: https://github.com/unifiedjs/unified-stream [remark-rehype]: https://github.com/remarkjs/remark-rehype @@ -1342,7 +1486,7 @@ work on [`ware`][ware], as it was a huge initial inspiration. [rehype-remark]: https://github.com/rehypejs/rehype-remark -[unist-utilities]: https://github.com/syntax-tree/unist#list-of-utilities +[node]: https://github.com/syntax-tree/unist#node [vfile]: https://github.com/vfile/vfile @@ -1350,31 +1494,25 @@ work on [`ware`][ware], as it was a huge initial inspiration. [vfile-utilities]: https://github.com/vfile/vfile#list-of-utilities -[node]: https://github.com/syntax-tree/unist#node - -[description]: #description - -[syntax-tree]: #syntax-trees - -[configuration]: #configuration +[overview]: #overview [file]: #file -[processors]: #processors +[api]: #api [process]: #processorprocessfile-done -[process-sync]: #processorprocesssyncfilevalue +[process-sync]: #processorprocesssyncfile [parse]: #processorparsefile [parser]: #processorparser -[stringify]: #processorstringifynode-file +[stringify]: #processorstringifytree-file -[run]: #processorrunnode-file-done +[run]: #processorruntree-file-done -[run-sync]: #processorrunsyncnode-file +[run-sync]: #processorrunsynctree-file [compiler]: #processorcompiler @@ -1382,7 +1520,7 @@ work on [`ware`][ware], as it was a huge initial inspiration. [attacher]: #function-attacheroptions -[transformer]: #function-transformernode-file-next +[transformer]: #function-transformertree-file-next [next]: #function-nexterr-tree-file @@ -1390,58 +1528,48 @@ work on [`ware`][ware], as it was a huge initial inspiration. [plugin]: #plugin -[run-done]: #function-doneerr-node-file +[run-done]: #function-doneerr-tree-file [process-done]: #function-doneerr-file [contribute]: #contribute +[sponsor]: #sponsor + [rehype-react]: https://github.com/rehypejs/rehype-react [trough]: https://github.com/wooorm/trough#function-fninput-next [promise]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise -[remark-plugins]: https://github.com/remarkjs/remark/blob/HEAD/doc/plugins.md#list-of-plugins - -[rehype-plugins]: https://github.com/rehypejs/rehype/blob/HEAD/doc/plugins.md#list-of-plugins - -[retext-plugins]: https://github.com/retextjs/retext/blob/HEAD/doc/plugins.md#list-of-plugins - -[stream]: https://github.com/unifiedjs/unified-stream - -[ideas]: https://github.com/unifiedjs/ideas +[remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins -[preliminary]: https://github.com/retextjs/retext/commit/8fcb1f#diff-168726dbe96b3ce427e7fedce31bb0bc - -[externalised]: https://github.com/remarkjs/remark/commit/9892ec#diff-168726dbe96b3ce427e7fedce31bb0bc - -[published]: https://github.com/unifiedjs/unified/commit/2ba1cf +[rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins -[ware]: https://github.com/segmentio/ware +[retext-plugins]: https://github.com/retextjs/retext/blob/main/doc/plugins.md#list-of-plugins -[gatsby]: https://www.gatsbyjs.org +[awesome-remark]: https://github.com/remarkjs/awesome-remark -[mdx]: https://mdxjs.com +[awesome-rehype]: https://github.com/rehypejs/awesome-rehype -[jsx]: https://reactjs.org/docs/jsx-in-depth.html +[awesome-retext]: https://github.com/retextjs/awesome-retext -[prettier]: https://prettier.io +[topic-remark-plugin]: https://github.com/topics/remark-plugin -[node.js]: https://nodejs.org +[topic-rehype-plugin]: https://github.com/topics/rehype-plugin -[vercel]: https://vercel.com +[topic-retext-plugin]: https://github.com/topics/retext-plugin -[netlify]: https://www.netlify.com +[types-hast]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/hast -[github]: https://github.com +[types-mdast]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mdast -[mozilla]: https://www.mozilla.org +[types-nlcst]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nlcst -[wordpress]: https://wordpress.com +[preliminary]: https://github.com/retextjs/retext/commit/8fcb1f#diff-168726dbe96b3ce427e7fedce31bb0bc -[adobe]: https://www.adobe.com +[externalised]: https://github.com/remarkjs/remark/commit/9892ec#diff-168726dbe96b3ce427e7fedce31bb0bc -[facebook]: https://www.facebook.com +[published]: https://github.com/unifiedjs/unified/commit/2ba1cf -[google]: https://www.google.com +[ware]: https://github.com/segmentio/ware From b8d42cb5d77d18cb9afb0464757c089850d9df13 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 12 Oct 2022 21:00:41 +0200 Subject: [PATCH 06/46] Update dev-dependencies --- .github/workflows/main.yml | 2 +- index.d.ts | 16 ++++++++-------- index.test-d.ts | 32 ++++++++++++++++---------------- package.json | 6 +++--- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8d1d2d12..faeb7752 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: - ubuntu-latest - windows-latest node: - - lts/erbium + - lts/fermium - lts/gallium canary: name: canary / ${{matrix.package}} / ${{matrix.node}} on ${{matrix.os}} diff --git a/index.d.ts b/index.d.ts index 09db6f2e..06762582 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,8 +13,8 @@ // Furthermore, this is places in the root of the project because types that // accept type parameters cannot be re-exported as such easily. -import {Node} from 'unist' -import {VFile, VFileCompatible} from 'vfile' +import type {Node} from 'unist' +import type {VFile, VFileCompatible} from 'vfile' /* eslint-disable @typescript-eslint/naming-convention */ @@ -86,12 +86,12 @@ type UsePlugin< * @typeParam CompileResult * The thing that the compiler yields. */ -export interface Processor< +export type Processor< ParseTree extends Node | void = void, CurrentTree extends Node | void = void, CompileTree extends Node | void = void, CompileResult = void -> extends FrozenProcessor { +> = { /** * Configure the processor to use a plugin. * @@ -205,7 +205,7 @@ export interface Processor< use( presetOrList: Preset | PluggableList ): Processor -} +} & FrozenProcessor /** * A frozen processor is just like a regular processor, except no additional @@ -213,12 +213,12 @@ export interface Processor< * A frozen processor can be created by calling `.freeze()` on a processor. * An unfrozen processor can be created by calling a processor. */ -export interface FrozenProcessor< +export type FrozenProcessor< ParseTree extends Node | void = void, CurrentTree extends Node | void = void, CompileTree extends Node | void = void, CompileResult = void -> { +> = { /** * Clone current processor * @@ -552,7 +552,7 @@ Input extends Node * Presets provide a sharable way to configure processors with multiple plugins * and/or settings. */ -export interface Preset { +export type Preset = { plugins?: PluggableList settings?: Record } diff --git a/index.test-d.ts b/index.test-d.ts index e5cf2211..292652ed 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -4,18 +4,18 @@ import type {Buffer} from 'node:buffer' import {expectType, expectError} from 'tsd' import type {Node, Parent, Literal} from 'unist' import type {VFile} from 'vfile' -import { - unified, +import type { Plugin, Processor, FrozenProcessor, TransformCallback } from './index.js' +import {unified} from './index.js' expectType(unified()) expectType(unified().freeze()) -interface ExamplePluginSettings { +type ExamplePluginSettings = { example: string } @@ -167,44 +167,44 @@ unified() // Plugins bound to a certain node. // A small subset of mdast. -interface MdastRoot extends Parent { +type MdastRoot = { type: 'root' children: MdastFlow[] -} +} & Parent type MdastFlow = MdastParagraph -interface MdastParagraph extends Parent { +type MdastParagraph = { type: 'paragraph' children: MdastPhrasing[] -} +} & Parent type MdastPhrasing = MdastText -interface MdastText extends Literal { +type MdastText = { type: 'text' value: string -} +} & Literal // A small subset of hast. -interface HastRoot extends Parent { +type HastRoot = { type: 'root' children: HastChild[] -} +} & Parent type HastChild = HastElement | HastText -interface HastElement extends Parent { +type HastElement = { type: 'element' tagName: string properties: Record children: HastChild[] -} +} & Parent -interface HastText extends Literal { +type HastText = { type: 'text' value: string -} +} & Literal const explicitPluginWithInputTree: Plugin = () => (tree, file) => { @@ -273,7 +273,7 @@ unified().use({ }) // Input and output types. -interface ReactNode { +type ReactNode = { kind: string } diff --git a/package.json b/package.json index 797e32ec..b08366ba 100644 --- a/package.json +++ b/package.json @@ -59,14 +59,14 @@ "@types/tape": "^4.0.0", "c8": "^7.0.0", "prettier": "^2.0.0", - "remark-cli": "^10.0.0", + "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "rimraf": "^3.0.0", "tape": "^5.0.0", - "tsd": "^0.21.0", + "tsd": "^0.24.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", - "xo": "^0.50.0" + "xo": "^0.52.0" }, "scripts": { "build": "rimraf \"test/**/*.d.ts\" && tsc && tsd && type-coverage", From 112342754777c9a637ce01f1fa74d1c97687cffa Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 17 Nov 2022 11:11:31 +0100 Subject: [PATCH 07/46] Update dev-dependencies --- index.test-d.ts | 20 ++++---------------- package.json | 2 +- test/freeze.js | 5 ++++- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index 292652ed..875e976f 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import type {Buffer} from 'node:buffer' import {expectType, expectError} from 'tsd' import type {Node, Parent, Literal} from 'unist' @@ -379,12 +377,7 @@ expectType( // Probably hast expected. expectError(unified().use(rehypeStringify).runSync(someMdast)) -unified() - .use(rehypeStringify) - .run(someHast) - .then((thing) => { - expectType(thing) - }) +expectType(await unified().use(rehypeStringify).run(someHast)) unified() .use(rehypeStringify) @@ -401,12 +394,9 @@ expectType( unified().use(remarkParse).use(rehypeReact).processSync('') ) -unified() - .use(rehypeReact) - .process('') - .then((file) => { - expectType(file) - }) +expectType( + await unified().use(rehypeReact).process('') +) unified() .use(rehypeReact) @@ -477,5 +467,3 @@ expectType( .use(explicitRehypePlugin) .runSync(someMdast) ) - -/* eslint-enable @typescript-eslint/no-floating-promises */ diff --git a/package.json b/package.json index b08366ba..208375ee 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "tsd": "^0.24.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", - "xo": "^0.52.0" + "xo": "^0.53.0" }, "scripts": { "build": "rimraf \"test/**/*.d.ts\" && tsc && tsd && type-coverage", diff --git a/test/freeze.js b/test/freeze.js index afcb4bf3..508b5693 100644 --- a/test/freeze.js +++ b/test/freeze.js @@ -78,7 +78,10 @@ test('freeze()', (t) => { processor().freeze() - /** @type {Plugin} */ + /** + * @this {import('../index.js').Processor} + * @type {Plugin} + */ function freezingPlugin() { this.freeze() } From f47ed4dbabcdce696530412bac2ba67b61839fa8 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 17 Nov 2022 11:13:38 +0100 Subject: [PATCH 08/46] Refactor `tsconfig.json` --- package.json | 8 ++++---- test/freeze.js | 2 +- test/process.js | 4 ++-- test/util/simple.js | 4 ++-- tsconfig.json | 17 +++++++++-------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 208375ee..ed551c53 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "prettier": "^2.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", - "rimraf": "^3.0.0", "tape": "^5.0.0", "tsd": "^0.24.0", "type-coverage": "^2.0.0", @@ -69,10 +68,11 @@ "xo": "^0.53.0" }, "scripts": { - "build": "rimraf \"test/**/*.d.ts\" && tsc && tsd && type-coverage", + "prepack": "npm run build && npm run format", + "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node --unhandled-rejections=strict --conditions development test/index.js", - "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --unhandled-rejections=strict --conditions development test/index.js", + "test-api": "node --conditions development test.js", + "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { diff --git a/test/freeze.js b/test/freeze.js index 508b5693..bba75d21 100644 --- a/test/freeze.js +++ b/test/freeze.js @@ -1,5 +1,5 @@ /** - * @typedef {import('..').Plugin} Plugin + * @typedef {import('../index.js').Plugin} Plugin */ import test from 'tape' diff --git a/test/process.js b/test/process.js index 3ba7f3e2..b048b86a 100644 --- a/test/process.js +++ b/test/process.js @@ -1,8 +1,8 @@ /** * @typedef {import('unist').Literal} Literal * @typedef {import('unist').Node} Node - * @typedef {import('..').Parser} Parser - * @typedef {import('..').Compiler} Compiler + * @typedef {import('../index.js').Parser} Parser + * @typedef {import('../index.js').Compiler} Compiler */ import {Buffer} from 'node:buffer' diff --git a/test/util/simple.js b/test/util/simple.js index 129c8b95..0d0cf1a7 100644 --- a/test/util/simple.js +++ b/test/util/simple.js @@ -1,7 +1,7 @@ /** * @typedef {import('unist').Literal} Literal - * @typedef {import('../..').Parser} Parser - * @typedef {import('../..').Compiler} Compiler + * @typedef {import('../../index.js').Parser} Parser + * @typedef {import('../../index.js').Compiler} Compiler */ /** @type {Parser} */ diff --git a/tsconfig.json b/tsconfig.json index 8767c241..38a7e9be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,17 @@ { - "include": ["lib/**/*.js", "test/**/*.js"], + "include": ["**/**.js", "index.d.ts"], + "exclude": ["coverage", "node_modules"], "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020"], - "module": "ES2020", - "moduleResolution": "node", - "allowJs": true, "checkJs": true, "declaration": true, "emitDeclarationOnly": true, - "allowSyntheticDefaultImports": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2020"], + "module": "node16", + "newLine": "lf", "skipLibCheck": true, - "strict": true + "strict": true, + "target": "es2020" } } From 5f4f1752db3739fa047accda23328a65886ae802 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 17 Nov 2022 11:15:43 +0100 Subject: [PATCH 09/46] Update actions --- .github/workflows/main.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index faeb7752..31dd3214 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,33 +3,33 @@ on: - pull_request - push jobs: - test: + main: name: test / unified / ${{matrix.node}} on ${{matrix.os}} runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 strategy: matrix: os: - ubuntu-latest - windows-latest node: - - lts/fermium - - lts/gallium + - lts/hydrogen + - node canary: name: canary / ${{matrix.package}} / ${{matrix.node}} on ${{matrix.os}} runs-on: ${{matrix.os}} steps: - name: checkout unified - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: setup node - uses: dcodeIO/setup-node-nvm@master + uses: actions/setup-node@v3 with: node-version: ${{matrix.node}} - name: setup unified @@ -38,7 +38,7 @@ jobs: npm install npm run build - name: checkout ${{matrix.package}} - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: ${{matrix.package}} path: canary/${{matrix.package}} @@ -55,7 +55,7 @@ jobs: os: - ubuntu-latest node: - - lts/gallium + - lts/hydrogen package: - unifiedjs/unified-engine - remarkjs/remark From ffb71ec6ef6fb534f8344d80c3b244b46beef9ab Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 17 Nov 2022 12:19:12 +0100 Subject: [PATCH 10/46] Use Node test runner --- package.json | 5 +- test/async-function.js | 17 +- test/core.js | 15 +- test/data.js | 19 +- test/freeze.js | 31 +- test/parse.js | 39 +- test/process.js | 73 ++-- test/run.js | 973 ++++++++++++++++++++++------------------- test/stringify.js | 37 +- test/use.js | 216 ++++----- 10 files changed, 729 insertions(+), 696 deletions(-) diff --git a/package.json b/package.json index ed551c53..d336e29b 100644 --- a/package.json +++ b/package.json @@ -56,12 +56,11 @@ }, "devDependencies": { "@types/extend": "^3.0.0", - "@types/tape": "^4.0.0", + "@types/node": "^18.0.0", "c8": "^7.0.0", "prettier": "^2.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", - "tape": "^5.0.0", "tsd": "^0.24.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", @@ -71,7 +70,7 @@ "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node --conditions development test.js", + "test-api": "node --conditions development test/index.js", "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, diff --git a/test/async-function.js b/test/async-function.js index 7f876e28..410c65bf 100644 --- a/test/async-function.js +++ b/test/async-function.js @@ -2,17 +2,16 @@ * @typedef {import('unist').Node} Node */ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {VFile} from 'vfile' import {unified} from '../index.js' -test('async function transformer () {}', (t) => { +test('async function transformer () {}', () => { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} const modifiedNode = {type: 'charlie'} - t.plan(5) - unified() .use(() => async function () {}) .use( @@ -24,13 +23,13 @@ test('async function transformer () {}', (t) => { } ) .use(() => async (tree, file) => { - t.equal(tree, givenNode, 'passes correct tree to an async function') - t.equal(file, givenFile, 'passes correct file to an async function') + assert.equal(tree, givenNode, 'passes correct tree to an async function') + assert.equal(file, givenFile, 'passes correct file to an async function') return modifiedNode }) .run(givenNode, givenFile, (error, tree, file) => { - t.error(error, 'should’t fail') - t.equal(tree, modifiedNode, 'passes given tree to `done`') - t.equal(file, givenFile, 'passes given file to `done`') + assert.ifError(error) + assert.equal(tree, modifiedNode, 'passes given tree to `done`') + assert.equal(file, givenFile, 'passes given file to `done`') }) }) diff --git a/test/core.js b/test/core.js index 0816d2e0..caa278eb 100644 --- a/test/core.js +++ b/test/core.js @@ -1,11 +1,12 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {unified} from '../index.js' -test('unified()', (t) => { +test('unified()', () => { /** @type {number} */ let count - t.throws( + assert.throws( () => { // @ts-expect-error: `use` does not exist on frozen processors. unified.use(() => {}) @@ -16,7 +17,7 @@ test('unified()', (t) => { const processor = unified() - t.equal(typeof processor, 'function', 'should return a function') + assert.equal(typeof processor, 'function', 'should return a function') processor.use(function () { count++ @@ -26,17 +27,15 @@ test('unified()', (t) => { count = 0 const otherProcessor = processor().freeze() - t.equal( + assert.equal( count, 1, 'should create a new processor implementing the ancestral processor when called (#1)' ) - t.equal( + assert.equal( otherProcessor.data('foo'), 'bar', 'should create a new processor implementing the ancestral processor when called (#2)' ) - - t.end() }) diff --git a/test/data.js b/test/data.js index 38016406..735c2877 100644 --- a/test/data.js +++ b/test/data.js @@ -1,40 +1,39 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {unified} from '../index.js' -test('data(key[, value])', (t) => { +test('data(key[, value])', () => { const processor = unified() - t.equal( + assert.equal( processor.data('foo', 'bar'), processor, 'should return self as setter' ) - t.equal(processor.data('foo'), 'bar', 'should return data as getter') + assert.equal(processor.data('foo'), 'bar', 'should return data as getter') - t.equal( + assert.equal( processor.data('toString'), null, 'should not return own inherited properties.' ) - t.deepEqual( + assert.deepEqual( processor.data(), {foo: 'bar'}, 'should return the memory without arguments' ) - t.deepEqual( + assert.deepEqual( processor.data({baz: 'qux'}), processor, 'should set the memory with just a value (#1)' ) - t.deepEqual( + assert.deepEqual( processor.data(), {baz: 'qux'}, 'should set the memory with just a value (#2)' ) - - t.end() }) diff --git a/test/freeze.js b/test/freeze.js index bba75d21..dca504bc 100644 --- a/test/freeze.js +++ b/test/freeze.js @@ -2,11 +2,12 @@ * @typedef {import('../index.js').Plugin} Plugin */ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {unified} from '../index.js' import {SimpleCompiler, SimpleParser} from './util/simple.js' -test('freeze()', (t) => { +test('freeze()', async (t) => { const frozen = unified() .use(function () { // Note: TS has a bug so setting `this.Parser` and such doesn’t work, @@ -19,11 +20,11 @@ test('freeze()', (t) => { .freeze() const unfrozen = frozen() - t.doesNotThrow(() => { + assert.doesNotThrow(() => { unfrozen.data() }, '`data` can be called on unfrozen interfaces') - t.throws( + assert.throws( () => { frozen.data('foo', 'bar') }, @@ -31,7 +32,7 @@ test('freeze()', (t) => { '`data` cannot be called on frozen interfaces' ) - t.throws( + assert.throws( () => { // @ts-expect-error: `use` does not exist on frozen processors. frozen.use() @@ -40,37 +41,35 @@ test('freeze()', (t) => { '`use` cannot be called on frozen interfaces' ) - t.doesNotThrow(() => { + assert.doesNotThrow(() => { frozen.parse() }, '`parse` can be called on frozen interfaces') - t.doesNotThrow(() => { + assert.doesNotThrow(() => { frozen.stringify({type: 'foo'}) }, '`stringify` can be called on frozen interfaces') - t.doesNotThrow(() => { + assert.doesNotThrow(() => { frozen.runSync({type: 'foo'}) }, '`runSync` can be called on frozen interfaces') - t.doesNotThrow(() => { + assert.doesNotThrow(() => { frozen.run({type: 'foo'}, () => {}) }, '`run` can be called on frozen interfaces') - t.doesNotThrow(() => { + assert.doesNotThrow(() => { frozen.processSync('') }, '`processSync` can be called on frozen interfaces') - t.doesNotThrow(() => { + assert.doesNotThrow(() => { frozen.process('', () => {}) }, '`process` can be called on frozen interfaces') - t.test('should freeze once, even for nested calls', (t) => { - t.plan(2) - + await t.test('should freeze once, even for nested calls', () => { let index = 0 const processor = unified() .use(() => { - t.pass('Expected: ' + String(index++)) + assert.ok(true, 'Expected: ' + String(index++)) }) .use({plugins: [freezingPlugin]}) .use({plugins: [freezingPlugin]}) @@ -86,6 +85,4 @@ test('freeze()', (t) => { this.freeze() } }) - - t.end() }) diff --git a/test/parse.js b/test/parse.js index ea8983ba..ec00bbd9 100644 --- a/test/parse.js +++ b/test/parse.js @@ -3,16 +3,15 @@ * @typedef {import('vfile').VFile} VFile */ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {unified} from '../index.js' -test('parse(file)', (t) => { +test('parse(file)', () => { const processor = unified() const givenNode = {type: 'delta'} - t.plan(15) - - t.throws( + assert.throws( () => { processor.parse('') }, @@ -21,40 +20,40 @@ test('parse(file)', (t) => { ) processor.Parser = function (doc, file) { - t.equal(typeof doc, 'string', 'should pass a document') - t.ok('message' in file, 'should pass a file') + assert.equal(typeof doc, 'string', 'should pass a document') + assert.ok('message' in file, 'should pass a file') } processor.Parser.prototype.parse = function () { - t.equal(arguments.length, 0, 'should not pass anything to `parse`') + assert.equal(arguments.length, 0, 'should not pass anything to `parse`') return givenNode } - t.equal( + assert.equal( processor.parse('charlie'), givenNode, 'should return the result `Parser#parse` returns' ) processor.Parser = function (doc, file) { - t.equal(typeof doc, 'string', 'should pass a document') - t.ok('message' in file, 'should pass a file') + assert.equal(typeof doc, 'string', 'should pass a document') + assert.ok('message' in file, 'should pass a file') return givenNode } - t.equal( + assert.equal( processor.parse('charlie'), givenNode, 'should return the result `Parser` returns if it’s not a constructor' ) processor.Parser = (doc, file) => { - t.equal(typeof doc, 'string', 'should pass a document') - t.ok('message' in file, 'should pass a file') + assert.equal(typeof doc, 'string', 'should pass a document') + assert.ok('message' in file, 'should pass a file') return givenNode } - t.equal( + assert.equal( processor.parse('charlie'), givenNode, 'should return the result `parser` returns if it’s an arrow function' @@ -66,22 +65,20 @@ test('parse(file)', (t) => { * @param {VFile} file */ constructor(doc, file) { - t.equal(typeof doc, 'string', 'should pass a document') - t.ok('message' in file, 'should pass a file') + assert.equal(typeof doc, 'string', 'should pass a document') + assert.ok('message' in file, 'should pass a file') } /** @returns {Node} */ parse() { - t.equal(arguments.length, 0, 'should not pass anything to `parse`') + assert.equal(arguments.length, 0, 'should not pass anything to `parse`') return givenNode } } - t.equal( + assert.equal( processor.parse('charlie'), givenNode, 'should return the result `Parser#parse` returns on an ES class' ) - - t.end() }) diff --git a/test/process.js b/test/process.js index b048b86a..af61ef2f 100644 --- a/test/process.js +++ b/test/process.js @@ -6,18 +6,17 @@ */ import {Buffer} from 'node:buffer' -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {VFile} from 'vfile' import {unified} from '../index.js' import {SimpleCompiler, SimpleParser} from './util/simple.js' -test('process(file, done)', (t) => { +test('process(file, done)', () => { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} - t.plan(11) - - t.throws( + assert.throws( () => { unified().process('') }, @@ -25,7 +24,7 @@ test('process(file, done)', (t) => { 'should throw without `Parser`' ) - t.throws( + assert.throws( () => { const processor = unified() processor.Parser = SimpleParser @@ -40,8 +39,8 @@ test('process(file, done)', (t) => { Object.assign(this, { /** @type {Parser} */ Parser(doc, file) { - t.equal(typeof doc, 'string', 'should pass `doc` to `Parser`') - t.equal(file, givenFile, 'should pass `file` to `Parser`') + assert.equal(typeof doc, 'string', 'should pass `doc` to `Parser`') + assert.equal(file, givenFile, 'should pass `file` to `Parser`') return givenNode } }) @@ -49,31 +48,31 @@ test('process(file, done)', (t) => { .use( () => function (tree, file) { - t.equal(tree, givenNode, 'should pass `tree` to transformers') - t.equal(file, givenFile, 'should pass `file` to transformers') + assert.equal(tree, givenNode, 'should pass `tree` to transformers') + assert.equal(file, givenFile, 'should pass `file` to transformers') } ) .use(function () { Object.assign(this, { /** @type {Compiler} */ Compiler(tree, file) { - t.equal(tree, givenNode, 'should pass `tree` to `Compiler`') - t.equal(file, givenFile, 'should pass `file` to `Compiler`') + assert.equal(tree, givenNode, 'should pass `tree` to `Compiler`') + assert.equal(file, givenFile, 'should pass `file` to `Compiler`') return 'charlie' } }) }) .process(givenFile, (error, file) => { - t.error(error, 'shouldn’t fail') + assert.ifError(error) - t.equal( + assert.equal( String(file), 'charlie', 'should store the result of `compile()` on `file`' ) }) - t.throws(() => { + assert.throws(() => { unified() .use(function () { Object.assign(this, {Parser: SimpleParser, Compiler: SimpleCompiler}) @@ -84,19 +83,17 @@ test('process(file, done)', (t) => { }, /^Error: Alfred$/) }) -test('process(file)', (t) => { +test('process(file)', () => { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} - t.plan(7) - unified() .use(function () { Object.assign(this, { /** @type {Parser} */ Parser(doc, file) { - t.equal(typeof doc, 'string', 'should pass `doc` to `Parser`') - t.equal(file, givenFile, 'should pass `file` to `Parser`') + assert.equal(typeof doc, 'string', 'should pass `doc` to `Parser`') + assert.equal(file, givenFile, 'should pass `file` to `Parser`') return givenNode } }) @@ -104,16 +101,16 @@ test('process(file)', (t) => { .use( () => function (tree, file) { - t.equal(tree, givenNode, 'should pass `tree` to transformers') - t.equal(file, givenFile, 'should pass `file` to transformers') + assert.equal(tree, givenNode, 'should pass `tree` to transformers') + assert.equal(file, givenFile, 'should pass `file` to transformers') } ) .use(function () { Object.assign(this, { /** @type {Compiler} */ Compiler(tree, file) { - t.equal(tree, givenNode, 'should pass `tree` to `Compiler`') - t.equal(file, givenFile, 'should pass `file` to `Compiler`') + assert.equal(tree, givenNode, 'should pass `tree` to `Compiler`') + assert.equal(file, givenFile, 'should pass `file` to `Compiler`') return 'charlie' } }) @@ -121,18 +118,16 @@ test('process(file)', (t) => { .process(givenFile) .then( (file) => { - t.equal(file.toString(), 'charlie', 'should resolve the file') + assert.equal(file.toString(), 'charlie', 'should resolve the file') }, () => { - t.fail('should resolve, not reject, the file') + assert.fail('should resolve, not reject, the file') } ) }) -test('processSync(file)', (t) => { - t.plan(4) - - t.throws( +test('processSync(file)', () => { + assert.throws( () => { unified().processSync('') }, @@ -140,7 +135,7 @@ test('processSync(file)', (t) => { 'should throw without `Parser`' ) - t.throws( + assert.throws( () => { const processor = unified() processor.Parser = SimpleParser @@ -150,7 +145,7 @@ test('processSync(file)', (t) => { 'should throw without `Compiler`' ) - t.throws( + assert.throws( () => { unified() .use(function () { @@ -165,7 +160,7 @@ test('processSync(file)', (t) => { 'should throw error from `processSync`' ) - t.equal( + assert.equal( unified() .use(function () { Object.assign(this, {Parser: SimpleParser, Compiler: SimpleCompiler}) @@ -181,10 +176,8 @@ test('processSync(file)', (t) => { ) }) -test('compilers', (t) => { - t.plan(4) - - t.equal( +test('compilers', () => { + assert.equal( unified() .use(function () { Object.assign(this, { @@ -199,7 +192,7 @@ test('compilers', (t) => { 'should compile strings' ) - t.deepEqual( + assert.deepEqual( unified() .use(function () { Object.assign(this, { @@ -214,7 +207,7 @@ test('compilers', (t) => { 'should compile buffers' ) - t.deepEqual( + assert.deepEqual( unified() .use(function () { Object.assign(this, { @@ -229,7 +222,7 @@ test('compilers', (t) => { 'should compile null' ) - t.deepEqual( + assert.deepEqual( unified() .use(function () { Object.assign(this, { diff --git a/test/run.js b/test/run.js index 357a1a0e..45f0284d 100644 --- a/test/run.js +++ b/test/run.js @@ -1,481 +1,543 @@ import process from 'node:process' -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {VFile} from 'vfile' import {unified} from '../index.js' -test('run(node[, file], done)', (t) => { +test('run(node[, file], done)', async () => { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} const otherNode = {type: 'delta'} - t.plan(21) - - unified().run(givenNode, givenFile, (error, tree, file) => { - t.error(error, 'should’t fail') - t.equal(tree, givenNode, 'passes given tree to `done`') - t.equal(file, givenFile, 'passes given file to `done`') + await new Promise((resolve) => { + unified().run(givenNode, givenFile, (error, tree, file) => { + assert.ifError(error) + assert.equal(tree, givenNode, 'passes given tree to `done`') + assert.equal(file, givenFile, 'passes given file to `done`') + resolve(undefined) + }) }) - unified().run(givenNode, undefined, (error, _, file) => { - t.error(error, 'should’t fail') - t.equal(String(file), '', 'passes file to `done` if not given') + await new Promise((resolve) => { + unified().run(givenNode, undefined, (error, _, file) => { + assert.ifError(error) + assert.equal(String(file), '', 'passes file to `done` if not given') + resolve(undefined) + }) }) - unified().run(givenNode, (error, _, file) => { - t.error(error, 'should’t fail') - t.equal(String(file), '', 'passes file to `done` if omitted') + await new Promise((resolve) => { + unified().run(givenNode, (error, _, file) => { + assert.ifError(error) + assert.equal(String(file), '', 'passes file to `done` if omitted') + resolve(undefined) + }) }) - unified() - .use( - () => - function () { - return new Error('charlie') - } - ) - .run(givenNode, (error) => { - t.equal( - String(error), - 'Error: charlie', - 'should pass an error to `done` from a sync transformer' + await new Promise((resolve) => { + unified() + .use( + () => + function () { + return new Error('charlie') + } ) - }) + .run(givenNode, (error) => { + assert.equal( + String(error), + 'Error: charlie', + 'should pass an error to `done` from a sync transformer' + ) + resolve(undefined) + }) + }) - unified() - .use(() => () => otherNode) - .run(givenNode, (error, tree) => { - t.error(error, 'should’t fail') + await new Promise((resolve) => { + unified() + .use(() => () => otherNode) + .run(givenNode, (error, tree) => { + assert.ifError(error) + + assert.equal( + tree, + otherNode, + 'should pass a new tree to `done`, when returned from a sync transformer' + ) + resolve(undefined) + }) + }) - t.equal( - tree, - otherNode, - 'should pass a new tree to `done`, when returned from a sync transformer' + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + next(new Error('delta')) + } ) - }) + .run(givenNode, (error) => { + assert.equal( + String(error), + 'Error: delta', + 'should pass an error to `done`, if given to a sync transformer’s `next`' + ) + resolve(undefined) + }) + }) - unified() - .use( - () => - function (_, _1, next) { - next(new Error('delta')) - } - ) - .run(givenNode, (error) => { - t.equal( - String(error), - 'Error: delta', - 'should pass an error to `done`, if given to a sync transformer’s `next`' + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + next() + next(new Error('delta')) + } ) - }) + .run(givenNode, (error) => { + assert.ifError(error) + resolve(undefined) + }) + }) - unified() - .use( - () => - function (_, _1, next) { - next() - next(new Error('delta')) - } - ) - .run(givenNode, (error) => { - t.error( - error, - 'should ignore multiple calls of `next` when called in a synchroneous transformer' + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + next(null, otherNode) + } ) - }) + .run(givenNode, (error, tree) => { + assert.ifError(error) - unified() - .use( - () => - function (_, _1, next) { - next(null, otherNode) - } - ) - .run(givenNode, (error, tree) => { - t.error(error, 'should’t fail') + assert.equal( + tree, + otherNode, + 'should pass a new tree to `done`, if given to a sync transformer’s `next`' + ) + resolve(undefined) + }) + }) - t.equal( - tree, - otherNode, - 'should pass a new tree to `done`, if given to a sync transformer’s `next`' + await new Promise((resolve) => { + unified() + .use( + () => + function () { + return new Promise((_, reject) => { + reject(new Error('delta')) + }) + } ) - }) + .run(givenNode, (error) => { + assert.equal( + String(error), + 'Error: delta', + 'should pass an error to `done` rejected from a sync transformer’s returned promise' + ) + resolve(undefined) + }) + }) - unified() - .use( - () => - function () { - return new Promise((_, reject) => { - reject(new Error('delta')) - }) - } - ) - .run(givenNode, (error) => { - t.equal( - String(error), - 'Error: delta', - 'should pass an error to `done` rejected from a sync transformer’s returned promise' + await new Promise((resolve) => { + unified() + .use( + // Note: TS JS doesn’t understand the promise w/o explicit type. + /** @type {import('../index.js').Plugin<[]>} */ + () => + function () { + return new Promise((resolve) => { + resolve(otherNode) + }) + } ) - }) + .run(givenNode, (error, tree) => { + assert.ifError(error) - unified() - .use( - // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('../index.js').Plugin<[]>} */ - () => - function () { - return new Promise((resolve) => { - resolve(otherNode) - }) - } - ) - .run(givenNode, (error, tree) => { - t.error(error, 'should’t fail') - - t.equal( - tree, - otherNode, - 'should pass a new tree to `done`, when resolved sync transformer’s returned promise' - ) - }) + assert.equal( + tree, + otherNode, + 'should pass a new tree to `done`, when resolved sync transformer’s returned promise' + ) + resolve(undefined) + }) + }) - unified() - .use( - () => - function (_, _1, next) { - setImmediate(tick) - function tick() { - next(null, otherNode) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + setImmediate(tick) + function tick() { + next(null, otherNode) + } } - } - ) - .run(givenNode, (error, tree) => { - t.error(error, 'should’t fail') - - t.equal( - tree, - otherNode, - 'should pass a new tree to `done` when given to `next` from an asynchroneous transformer' ) - }) + .run(givenNode, (error, tree) => { + assert.ifError(error) - unified() - .use( - () => - function (_, _1, next) { - setImmediate(tick) - function tick() { - next(new Error('echo')) + assert.equal( + tree, + otherNode, + 'should pass a new tree to `done` when given to `next` from an asynchroneous transformer' + ) + resolve(undefined) + }) + }) + + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + setImmediate(tick) + function tick() { + next(new Error('echo')) + } } - } - ) - .run(givenNode, (error) => { - t.equal( - String(error), - 'Error: echo', - 'should pass an error to `done` given to `next` from an asynchroneous transformer' ) - }) + .run(givenNode, (error) => { + assert.equal( + String(error), + 'Error: echo', + 'should pass an error to `done` given to `next` from an asynchroneous transformer' + ) + resolve(undefined) + }) + }) - unified() - .use( - () => - function (_, _1, next) { - setImmediate(tick) - function tick() { - next() - next(new Error('echo')) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + setImmediate(tick) + function tick() { + next() + next(new Error('echo')) + } } - } - ) - .run(givenNode, (error) => { - t.error( - error, - 'should ignore multiple calls of `next` when called from an asynchroneous transformer' ) - }) + .run(givenNode, (error) => { + assert.ifError(error) + resolve(undefined) + }) + }) }) -test('run(node[, file])', (t) => { +test('run(node[, file])', async () => { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} const otherNode = {type: 'delta'} - t.plan(13) - - unified() - .run(givenNode, givenFile) - .then( - (tree) => { - t.equal(tree, givenNode, 'should resolve the given tree') - }, - () => { - t.fail('should resolve, not reject, when `file` is given') - } - ) + await new Promise((resolve) => { + unified() + .run(givenNode, givenFile) + .then( + (tree) => { + assert.equal(tree, givenNode, 'should resolve the given tree') + resolve(undefined) + }, + () => { + assert.fail('should resolve, not reject, when `file` is given') + resolve(undefined) + } + ) + }) - unified() - .run(givenNode, undefined) - .then( - (tree) => { - t.equal(tree, givenNode, 'should work if `file` is not given') - }, - () => { - t.fail('should resolve, not reject, when `file` is not given') - } - ) + await new Promise((resolve) => { + unified() + .run(givenNode, undefined) + .then( + (tree) => { + assert.equal(tree, givenNode, 'should work if `file` is not given') + resolve(undefined) + }, + () => { + assert.fail('should resolve, not reject, when `file` is not given') + resolve(undefined) + } + ) + }) - unified() - .run(givenNode) - .then( - (tree) => { - t.equal(tree, givenNode, 'should work if `file` is omitted') - }, - () => { - t.fail('should resolve, not reject, when `file` is omitted') - } - ) + await new Promise((resolve) => { + unified() + .run(givenNode) + .then( + (tree) => { + assert.equal(tree, givenNode, 'should work if `file` is omitted') + resolve(undefined) + }, + () => { + assert.fail('should resolve, not reject, when `file` is omitted') + resolve(undefined) + } + ) + }) - unified() - .use( - () => - function () { - return new Error('charlie') + await new Promise((resolve) => { + unified() + .use( + () => + function () { + return new Error('charlie') + } + ) + .run(givenNode) + .then( + () => { + assert.fail( + 'should reject, not resolve, when an error is passed to `done` from a sync transformer' + ) + resolve(undefined) + }, + (/** @type {Error} */ error) => { + assert.equal( + String(error), + 'Error: charlie', + 'should reject when an error is returned from a sync transformer' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.fail( - 'should reject, not resolve, when an error is passed to `done` from a sync transformer' - ) - }, - (/** @type {Error} */ error) => { - t.equal( - String(error), - 'Error: charlie', - 'should reject when an error is returned from a sync transformer' - ) - } - ) + ) + }) - unified() - .use( - () => - function () { - return otherNode + await new Promise((resolve) => { + unified() + .use( + () => + function () { + return otherNode + } + ) + .run(givenNode) + .then( + (tree) => { + assert.equal( + tree, + otherNode, + 'should resolve a new tree when returned from a sync transformer' + ) + resolve(undefined) + }, + () => { + assert.fail( + 'should resolve, not reject, when a new tree is given from a sync transformer' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - (tree) => { - t.equal( - tree, - otherNode, - 'should resolve a new tree when returned from a sync transformer' - ) - }, - () => { - t.fail( - 'should resolve, not reject, when a new tree is given from a sync transformer' - ) - } - ) + ) + }) - unified() - .use( - () => - function (_, _1, next) { - next(new Error('delta')) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + next(new Error('delta')) + } + ) + .run(givenNode) + .then( + () => { + assert.fail( + 'should reject, not resolve, if an error is given to a sync transformer’s `next`' + ) + resolve(undefined) + }, + (/** @type {Error} */ error) => { + assert.equal( + String(error), + 'Error: delta', + 'should reject, if an error is given to a sync transformer’s `next`' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.fail( - 'should reject, not resolve, if an error is given to a sync transformer’s `next`' - ) - }, - (/** @type {Error} */ error) => { - t.equal( - String(error), - 'Error: delta', - 'should reject, if an error is given to a sync transformer’s `next`' - ) - } - ) + ) + }) - unified() - .use( - () => - function (_, _1, next) { - next() - next(new Error('delta')) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + next() + next(new Error('delta')) + } + ) + .run(givenNode) + .then( + function () { + resolve(undefined) + }, + () => { + assert.fail( + 'should ignore multiple calls of `next` when called in a synchroneous transformer' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.pass( - 'should ignore multiple calls of `next` when called in a synchroneous transformer' - ) - }, - () => { - t.fail( - 'should ignore multiple calls of `next` when called in a synchroneous transformer' - ) - } - ) + ) + }) - unified() - .use( - () => - function (_, _1, next) { - next(null, otherNode) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + next(null, otherNode) + } + ) + .run(givenNode) + .then( + (tree) => { + assert.equal( + tree, + otherNode, + 'should resolve if a new tree is given to a sync transformer’s `next`' + ) + resolve(undefined) + }, + () => { + assert.fail( + 'should resolve, not reject, if a new tree is given to a sync transformer’s `next`' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - (tree) => { - t.equal( - tree, - otherNode, - 'should resolve if a new tree is given to a sync transformer’s `next`' - ) - }, - () => { - t.fail( - 'should resolve, not reject, if a new tree is given to a sync transformer’s `next`' - ) - } - ) + ) + }) - unified() - .use( - () => + await new Promise((resolve) => { + unified() + .use( + () => + function () { + return new Promise((_, reject) => { + reject(new Error('delta')) + }) + } + ) + .run(givenNode) + .then( + () => { + assert.fail( + 'should reject, not resolve, if an error is rejected from a sync transformer’s returned promise' + ) + resolve(undefined) + }, function () { - return new Promise((_, reject) => { - reject(new Error('delta')) - }) + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.fail( - 'should reject, not resolve, if an error is rejected from a sync transformer’s returned promise' - ) - }, - () => { - t.pass( - 'should reject if an error is rejected from a sync transformer’s returned promise' - ) - } - ) + ) + }) - unified() - .use( - // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('../index.js').Plugin<[]>} */ - () => + await new Promise((resolve) => { + unified() + .use( + // Note: TS JS doesn’t understand the promise w/o explicit type. + /** @type {import('../index.js').Plugin<[]>} */ + () => + function () { + return new Promise((resolve) => { + resolve(otherNode) + }) + } + ) + .run(givenNode) + .then( function () { - return new Promise((resolve) => { - resolve(otherNode) - }) + resolve(undefined) + }, + () => { + assert.fail( + 'should resolve, not reject, a new tree if it’s resolved from a sync transformer’s returned promise' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.pass( - 'should resolve a new tree if it’s resolved from a sync transformer’s returned promise' - ) - }, - () => { - t.fail( - 'should resolve, not reject, a new tree if it’s resolved from a sync transformer’s returned promise' - ) - } - ) + ) + }) - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next(null, otherNode) - }) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + setImmediate(() => { + next(null, otherNode) + }) + } + ) + .run(givenNode) + .then( + function () { + resolve(undefined) + }, + () => { + assert.fail( + 'should resolve, not reject, if a new tree is given to `next` from an asynchroneous transformer' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.pass( - 'should resolve a new tree if it’s given to `next` from an asynchroneous transformer' - ) - }, - () => { - t.fail( - 'should resolve, not reject, if a new tree is given to `next` from an asynchroneous transformer' - ) - } - ) + ) + }) - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next(new Error('echo')) - }) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + setImmediate(() => { + next(new Error('echo')) + }) + } + ) + .run(givenNode) + .then( + () => { + assert.fail( + 'should reject, not resolve, if an error is given to `next` from an asynchroneous transformer' + ) + resolve(undefined) + }, + function () { + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.fail( - 'should reject, not resolve, if an error is given to `next` from an asynchroneous transformer' - ) - }, - () => { - t.pass( - 'should reject if an error is given to `next` from an asynchroneous transformer' - ) - } - ) + ) + }) - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next() - next(new Error('echo')) - }) + await new Promise((resolve) => { + unified() + .use( + () => + function (_, _1, next) { + setImmediate(() => { + next() + next(new Error('echo')) + }) + } + ) + .run(givenNode) + .then( + function () { + resolve(undefined) + }, + () => { + assert.fail( + 'should ignore multiple calls of `next` when called from an asynchroneous transformer' + ) + resolve(undefined) } - ) - .run(givenNode) - .then( - () => { - t.pass( - 'should ignore multiple calls of `next` when called from an asynchroneous transformer' - ) - }, - () => { - t.fail( - 'should ignore multiple calls of `next` when called from an asynchroneous transformer' - ) - } - ) + ) + }) }) -test('runSync(node[, file])', (t) => { +test('runSync(node[, file])', async () => { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} const otherNode = {type: 'delta'} - t.plan(12) - - t.throws( + assert.throws( () => { // @ts-expect-error: `node` is required. unified().runSync() @@ -488,8 +550,8 @@ test('runSync(node[, file])', (t) => { .use( () => function (tree, file) { - t.equal(tree, givenNode, 'passes given tree to transformers') - t.equal(file, givenFile, 'passes given file to transformers') + assert.equal(tree, givenNode, 'passes given tree to transformers') + assert.equal(file, givenFile, 'passes given file to transformers') } ) .runSync(givenNode, givenFile) @@ -498,7 +560,7 @@ test('runSync(node[, file])', (t) => { .use( () => function (_, file) { - t.equal( + assert.equal( file.toString(), '', 'passes files to transformers if not given' @@ -507,7 +569,7 @@ test('runSync(node[, file])', (t) => { ) .runSync(givenNode) - t.throws( + assert.throws( () => { unified() .use( @@ -522,7 +584,7 @@ test('runSync(node[, file])', (t) => { 'should throw an error returned from a sync transformer' ) - t.equal( + assert.equal( unified() .use( () => @@ -535,7 +597,7 @@ test('runSync(node[, file])', (t) => { 'should return a new tree when returned from a sync transformer' ) - t.throws( + assert.throws( () => { unified() .use( @@ -550,7 +612,7 @@ test('runSync(node[, file])', (t) => { 'should throw an error if given to a sync transformer’s `next`' ) - t.equal( + assert.equal( unified() .use( () => @@ -563,24 +625,38 @@ test('runSync(node[, file])', (t) => { 'should return a new tree if given to a sync transformer’s `next`' ) - t.throws( - () => { - unified() - .use( - () => - function () { - return new Promise((_, reject) => { - reject(new Error('delta')) - }) - } - ) - .runSync(givenNode) - }, - /`runSync` finished async. Use `run` instead/, - 'should not support a promise returning transformer rejecting in `runSync`' - ) + await new Promise((resolve) => { + /** @type {unknown} */ + // @ts-expect-error: prevent the test runner from warning. + const current = process._events.unhandledRejection + // @ts-expect-error: prevent the test runner from warning. + process._events.unhandledRejection = undefined + + process.once('unhandledRejection', function () { + resolve(undefined) + // @ts-expect-error: prevent the test runner from warning. + process._events.unhandledRejection = current + }) - t.throws( + assert.throws( + () => { + unified() + .use( + () => + function () { + return new Promise((_, reject) => { + reject(new Error('delta')) + }) + } + ) + .runSync(givenNode) + }, + /`runSync` finished async. Use `run` instead/, + 'should not support a promise returning transformer rejecting in `runSync`' + ) + }) + + assert.throws( () => { unified() .use( @@ -599,26 +675,25 @@ test('runSync(node[, file])', (t) => { 'should not support a promise returning transformer resolving in `runSync`' ) - process.on('uncaughtException', () => { - t.pass( - 'should throw the actual error from a rejecting transformer in `runSync`' + await new Promise((resolve) => { + assert.throws( + () => { + unified() + .use( + () => + function (_, _1, next) { + setImmediate(() => { + next(null, otherNode) + setImmediate(() => { + resolve(undefined) + }) + }) + } + ) + .runSync(givenNode) + }, + /`runSync` finished async. Use `run` instead/, + 'should throw an error if an asynchroneous transformer is used but no `done` is given' ) }) - - t.throws( - () => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next(null, otherNode) - }) - } - ) - .runSync(givenNode) - }, - /`runSync` finished async. Use `run` instead/, - 'should throw an error if an asynchroneous transformer is used but no `done` is given' - ) }) diff --git a/test/stringify.js b/test/stringify.js index 76b5cc9e..9e9ec1c9 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -2,18 +2,17 @@ * @typedef {import('unist').Node} Node */ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {VFile} from 'vfile' import {unified} from '../index.js' -test('stringify(node[, file])', (t) => { +test('stringify(node[, file])', () => { const processor = unified() const givenFile = new VFile('charlie') const givenNode = {type: 'delta'} - t.plan(15) - - t.throws( + assert.throws( () => { processor.stringify({type: 'x'}) }, @@ -22,42 +21,42 @@ test('stringify(node[, file])', (t) => { ) processor.Compiler = function (node, file) { - t.equal(node, givenNode, 'should pass a node') - t.ok('message' in file, 'should pass a file') + assert.equal(node, givenNode, 'should pass a node') + assert.ok('message' in file, 'should pass a file') } // `prototype`s are objects. // type-coverage:ignore-next-line processor.Compiler.prototype.compile = function () { - t.equal(arguments.length, 0, 'should not pass anything to `compile`') + assert.equal(arguments.length, 0, 'should not pass anything to `compile`') return 'echo' } - t.equal( + assert.equal( processor.stringify(givenNode, givenFile), 'echo', 'should return the result `Compiler#compile` returns' ) processor.Compiler = function (node, file) { - t.equal(node, givenNode, 'should pass a node') - t.ok('message' in file, 'should pass a file') + assert.equal(node, givenNode, 'should pass a node') + assert.ok('message' in file, 'should pass a file') return 'echo' } - t.equal( + assert.equal( processor.stringify(givenNode, givenFile), 'echo', 'should return the result `compiler` returns if it’s not a constructor' ) processor.Compiler = (node, file) => { - t.equal(node, givenNode, 'should pass a node') - t.ok('message' in file, 'should pass a file') + assert.equal(node, givenNode, 'should pass a node') + assert.ok('message' in file, 'should pass a file') return 'echo' } - t.equal( + assert.equal( processor.stringify(givenNode, givenFile), 'echo', 'should return the result `compiler` returns if it’s an arrow function' @@ -69,17 +68,17 @@ test('stringify(node[, file])', (t) => { * @param {VFile} file */ constructor(node, file) { - t.equal(node, givenNode, 'should pass a node') - t.ok('message' in file, 'should pass a file') + assert.equal(node, givenNode, 'should pass a node') + assert.ok('message' in file, 'should pass a file') } compile() { - t.equal(arguments.length, 0, 'should not pass anything to `compile`') + assert.equal(arguments.length, 0, 'should not pass anything to `compile`') return 'echo' } } - t.equal( + assert.equal( processor.stringify(givenNode, givenFile), 'echo', 'should return the result `Compiler#compile` returns on an ES class' diff --git a/test/use.js b/test/use.js index 1a3eb423..e3fa5913 100644 --- a/test/use.js +++ b/test/use.js @@ -1,20 +1,20 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' import {unified} from '../index.js' -test('use(plugin[, options])', (t) => { - t.test('should ignore missing values', (t) => { +test('use(plugin[, options])', async (t) => { + await t.test('should ignore missing values', () => { const processor = unified() // @ts-expect-error: runtime feature. - t.equal(processor.use(), processor, 'missing') + assert.equal(processor.use(), processor, 'missing') // @ts-expect-error: runtime feature. - t.equal(processor.use(null), processor, '`null`') + assert.equal(processor.use(null), processor, '`null`') // @ts-expect-error: runtime feature. - t.equal(processor.use(undefined), processor, '`undefined`') - t.end() + assert.equal(processor.use(undefined), processor, '`undefined`') }) - t.test('should throw when given invalid values', (t) => { - t.throws( + await t.test('should throw when given invalid values', () => { + assert.throws( () => { // @ts-expect-error: runtime. unified().use(false) @@ -23,7 +23,7 @@ test('use(plugin[, options])', (t) => { '`false`' ) - t.throws( + assert.throws( () => { // @ts-expect-error: runtime. unified().use(true) @@ -32,7 +32,7 @@ test('use(plugin[, options])', (t) => { '`true`' ) - t.throws( + assert.throws( () => { // @ts-expect-error: runtime. unified().use('alfred') @@ -40,71 +40,65 @@ test('use(plugin[, options])', (t) => { /^TypeError: Expected usable value, not `alfred`$/, '`string`' ) - - t.end() }) - t.test('should support plugin and options', (t) => { + await t.test('should support plugin and options', () => { const processor = unified() const givenOptions = {} - t.plan(2) - processor .use(function (options) { - t.equal( + assert.equal( this, processor, 'should call a plugin with `processor` as the context' ) - t.equal(options, givenOptions, 'should call a plugin with `options`') + assert.equal( + options, + givenOptions, + 'should call a plugin with `options`' + ) }, givenOptions) .freeze() }) - t.test('should support a list of plugins', (t) => { + await t.test('should support a list of plugins', () => { const processor = unified() - t.plan(2) - processor .use([ function () { - t.equal(this, processor, 'should support a list of plugins (#1)') + assert.equal(this, processor, 'should support a list of plugins (#1)') }, function () { - t.equal(this, processor, 'should support a list of plugins (#2)') + assert.equal(this, processor, 'should support a list of plugins (#2)') } ]) .freeze() }) - t.test('should support a list of one plugin', (t) => { + await t.test('should support a list of one plugin', () => { const processor = unified() - t.plan(1) - processor .use([ function () { - t.equal(this, processor, 'should support a list of plugins (#2)') + assert.equal(this, processor, 'should support a list of plugins (#2)') } ]) .freeze() }) - t.test('should support a list of plugins and arguments', (t) => { + await t.test('should support a list of plugins and arguments', () => { const processor = unified() const givenOptions = {} - t.plan(2) - processor .use([ [ /** @param {unknown} options */ function (options) { - t.equal( + assert.equal( options, givenOptions, 'should support arguments with options' @@ -114,7 +108,7 @@ test('use(plugin[, options])', (t) => { ], [ function () { - t.equal( + assert.equal( this, processor, 'should support a arguments without options' @@ -125,8 +119,8 @@ test('use(plugin[, options])', (t) => { .freeze() }) - t.test('should throw when given invalid values in lists', (t) => { - t.throws( + await t.test('should throw when given invalid values in lists', () => { + assert.throws( () => { // @ts-expect-error: runtime. unified().use([false]) @@ -135,7 +129,7 @@ test('use(plugin[, options])', (t) => { '`false`' ) - t.throws( + assert.throws( () => { // @ts-expect-error: runtime. unified().use([true]) @@ -144,7 +138,7 @@ test('use(plugin[, options])', (t) => { '`true`' ) - t.throws( + assert.throws( () => { // @ts-expect-error: runtime. unified().use(['alfred']) @@ -152,16 +146,12 @@ test('use(plugin[, options])', (t) => { /^TypeError: Expected usable value, not `alfred`$/, '`string`' ) - - t.end() }) - t.test('should reconfigure objects', (t) => { + await t.test('should reconfigure objects', () => { const leftOptions = {foo: true, bar: true} const rightOptions = {foo: false, qux: true} - t.plan(4) - unified().use(change, 'this').use(change, rightOptions).freeze() unified().use(change).use(change, rightOptions).freeze() unified().use(change, [1, 2, 3]).use(change, rightOptions).freeze() @@ -169,12 +159,16 @@ test('use(plugin[, options])', (t) => { /** @param {unknown} [options] */ function change(options) { - t.deepEqual(options, {foo: false, qux: true}, 'should reconfigure (set)') + assert.deepEqual( + options, + {foo: false, qux: true}, + 'should reconfigure (set)' + ) } /** @param {Record} options */ function merge(options) { - t.deepEqual( + assert.deepEqual( options, {foo: false, bar: true, qux: true}, 'should reconfigure (merge)' @@ -182,9 +176,7 @@ test('use(plugin[, options])', (t) => { } }) - t.test('should reconfigure strings', (t) => { - t.plan(4) - + await t.test('should reconfigure strings', () => { unified().use(plugin, 'this').use(plugin, 'that').freeze() unified().use(plugin).use(plugin, 'that').freeze() unified().use(plugin, [1, 2, 3]).use(plugin, 'that').freeze() @@ -192,13 +184,11 @@ test('use(plugin[, options])', (t) => { /** @param {unknown} [options] */ function plugin(options) { - t.equal(options, 'that', 'should reconfigure') + assert.equal(options, 'that', 'should reconfigure') } }) - t.test('should reconfigure arrays', (t) => { - t.plan(4) - + await t.test('should reconfigure arrays', () => { unified().use(plugin, [1, 2, 3]).use(plugin, [4, 5, 6]).freeze() unified().use(plugin).use(plugin, [4, 5, 6]).freeze() unified().use(plugin, {foo: 'true'}).use(plugin, [4, 5, 6]).freeze() @@ -206,28 +196,25 @@ test('use(plugin[, options])', (t) => { /** @param {unknown} [options] */ function plugin(options) { - t.deepEqual(options, [4, 5, 6], 'should reconfigure') + assert.deepEqual(options, [4, 5, 6], 'should reconfigure') } }) - t.test('should reconfigure to turn off', (t) => { + await t.test('should reconfigure to turn off', () => { const processor = unified() - t.doesNotThrow(() => { + assert.doesNotThrow(() => { processor.use([[plugin], [plugin, false]]).freeze() function plugin() { throw new Error('Error') } }) - - t.end() }) - t.test('should reconfigure to turn on (boolean)', (t) => { + await t.test('should reconfigure to turn on (boolean)', () => { const processor = unified() - - t.plan(1) + let called = false processor .use([ @@ -236,16 +223,16 @@ test('use(plugin[, options])', (t) => { ]) .freeze() + assert.ok(called, 'should reconfigure') + function plugin() { - t.pass('should reconfigure') + called = true } }) - t.test('should reconfigure to turn on (options)', (t) => { + await t.test('should reconfigure to turn on (options)', () => { const processor = unified() - t.plan(1) - processor .use([ [plugin, false], @@ -255,21 +242,19 @@ test('use(plugin[, options])', (t) => { /** @param {unknown} [options] */ function plugin(options) { - t.deepEqual(options, {foo: true}, 'should reconfigure') + assert.deepEqual(options, {foo: true}, 'should reconfigure') } }) - t.test('should attach transformers', (t) => { + await t.test('should attach transformers', () => { const processor = unified() const givenNode = {type: 'test'} const condition = true - t.plan(3) - processor .use(() => (node, file) => { - t.equal(node, givenNode, 'should attach a transformer (#1)') - t.ok('message' in file, 'should attach a transformer (#2)') + assert.equal(node, givenNode, 'should attach a transformer (#1)') + assert.ok('message' in file, 'should attach a transformer (#2)') if (condition) { throw new Error('Alpha bravo charlie') @@ -277,7 +262,7 @@ test('use(plugin[, options])', (t) => { }) .freeze() - t.throws( + assert.throws( () => { processor.runSync(givenNode) }, @@ -285,12 +270,10 @@ test('use(plugin[, options])', (t) => { 'should attach a transformer (#3)' ) }) - - t.end() }) -test('use(preset)', (t) => { - t.throws( +test('use(preset)', async (t) => { + assert.throws( () => { // @ts-expect-error: runtime. unified().use({plugins: false}) @@ -299,7 +282,7 @@ test('use(preset)', (t) => { 'should throw on invalid `plugins` (1)' ) - t.throws( + assert.throws( () => { // @ts-expect-error: runtime. unified().use({plugins: {foo: true}}) @@ -308,94 +291,91 @@ test('use(preset)', (t) => { 'should throw on invalid `plugins` (2)' ) - t.test('should support empty presets', (t) => { + await t.test('should support empty presets', () => { const processor = unified().use({}).freeze() - t.equal(processor.attachers.length, 0) - t.end() + assert.equal(processor.attachers.length, 0) }) - t.test('should support presets with empty plugins', (t) => { + await t.test('should support presets with empty plugins', () => { const processor = unified().use({plugins: []}).freeze() - t.equal(processor.attachers.length, 0) - t.end() + assert.equal(processor.attachers.length, 0) }) - t.test('should support presets with empty settings', (t) => { + await t.test('should support presets with empty settings', () => { const processor = unified().use({settings: {}}).freeze() - t.deepEqual(processor.data(), {settings: {}}) - t.end() + assert.deepEqual(processor.data(), {settings: {}}) }) - t.test('should support presets with a plugin', (t) => { - t.plan(2) - + await t.test('should support presets with a plugin', () => { + let called = false const processor = unified() .use({plugins: [plugin]}) .freeze() - t.equal(processor.attachers.length, 1) + assert.equal(processor.attachers.length, 1) + assert.ok(called) function plugin() { - t.pass() + called = true } }) - t.test('should support presets with plugins', (t) => { + await t.test('should support presets with plugins', () => { + let calls = 0 const processor = unified() .use({plugins: [plugin1, plugin2]}) .freeze() - t.plan(3) - t.equal(processor.attachers.length, 2) + assert.equal(processor.attachers.length, 2) + assert.equal(calls, 2) function plugin1() { - t.pass() + calls++ } function plugin2() { - t.pass() + calls++ } }) - t.test('should support presets with settings', (t) => { + await t.test('should support presets with settings', () => { const processor = unified() .use({settings: {foo: true}}) .freeze() - t.deepEqual(processor.data('settings'), {foo: true}) - t.end() + assert.deepEqual(processor.data('settings'), {foo: true}) }) - t.test('should merge multiple presets with settings', (t) => { + await t.test('should merge multiple presets with settings', () => { const data = unified() .use({settings: {foo: true, bar: true}}) .use({settings: {qux: true, foo: false}}) .data() - t.deepEqual(data.settings, {foo: false, bar: true, qux: true}) - t.end() + assert.deepEqual(data.settings, {foo: false, bar: true, qux: true}) }) - t.test('should support extending presets', (t) => { + await t.test('should support extending presets', () => { + let calls = 0 const processor = unified() .use({settings: {alpha: true}, plugins: [plugin1, plugin2]}) .freeze() const otherProcessor = processor().freeze() - t.plan(7) - t.equal(processor.attachers.length, 2, '1') - t.equal(otherProcessor.attachers.length, 2, '2') - t.deepEqual(otherProcessor.data('settings'), {alpha: true}, '3') + assert.equal(processor.attachers.length, 2, '1') + assert.equal(otherProcessor.attachers.length, 2, '2') + assert.deepEqual(otherProcessor.data('settings'), {alpha: true}, '3') + assert.equal(calls, 4) function plugin1() { - t.pass('a') + calls++ } function plugin2() { - t.pass('b') + calls++ } }) - t.test('should support presets with plugins as a matrix', (t) => { + await t.test('should support presets with plugins as a matrix', () => { const one = {} const two = {} const processor = unified() @@ -408,26 +388,25 @@ test('use(preset)', (t) => { .freeze() const otherProcessor = processor().freeze() - t.plan(6) - t.equal(processor.attachers.length, 2, '1') - t.equal(otherProcessor.attachers.length, 2, '2') + assert.equal(processor.attachers.length, 2, '1') + assert.equal(otherProcessor.attachers.length, 2, '2') /** * @param {unknown} options */ function plugin1(options) { - t.equal(options, one, 'a') + assert.equal(options, one, 'a') } /** * @param {unknown} options */ function plugin2(options) { - t.equal(options, two, 'b') + assert.equal(options, two, 'b') } }) - t.test('should support nested presets', (t) => { + await t.test('should support nested presets', () => { const one = {} const two = {} const processor = unified() @@ -437,20 +416,17 @@ test('use(preset)', (t) => { .freeze() const otherProcessor = processor().freeze() - t.plan(6) - t.equal(processor.attachers.length, 2, '1') - t.equal(otherProcessor.attachers.length, 2, '2') + assert.equal(processor.attachers.length, 2, '1') + assert.equal(otherProcessor.attachers.length, 2, '2') /** @param {unknown} [options] */ function plugin1(options) { - t.equal(options, one, 'a') + assert.equal(options, one, 'a') } /** @param {unknown} [options] */ function plugin2(options) { - t.equal(options, two, 'b') + assert.equal(options, two, 'b') } }) - - t.end() }) From 2d954519648608adb496f47d3192cd776be099ea Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 17 Nov 2022 12:20:06 +0100 Subject: [PATCH 11/46] Refactor some docs --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 987cfdc3..bedee284 100644 --- a/readme.md +++ b/readme.md @@ -81,7 +81,7 @@ you need. ## Install This package is [ESM only][esm]. -In Node.js (version 12.20+, 14.14+, 16.0+, 18.0+), install with [npm][]: +In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: ```sh npm install unified @@ -1289,7 +1289,7 @@ export function rehypeStringify(options) {} Projects maintained by the unified collective are compatible with all maintained versions of Node.js. -As of now, that is Node.js 12.20+, 14.14+, 16.0+, and 18.0+. +As of now, that is Node.js 12.20+, 14.14+, and 16.0+. Our projects sometimes work with older versions, but this is not guaranteed. ## Contribute From a06537ccb16eca7d393e96d7039aec9c0bec4ba8 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 17 May 2023 17:28:33 +0200 Subject: [PATCH 12/46] Add sponsor --- readme.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index bedee284..7a604451 100644 --- a/readme.md +++ b/readme.md @@ -1360,11 +1360,14 @@ Support this effort and give back by sponsoring on [OpenCollective][collective]! Boost Note

+ + Markdown Space

+ + Holloway

- From 4a94722b483cbc4ee41f30121af1cdf4fe53ec92 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 17 May 2023 17:29:31 +0200 Subject: [PATCH 13/46] Fix build --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d336e29b..b4a84fef 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "tsd": "^0.24.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", - "xo": "^0.53.0" + "xo": "^0.54.0" }, "scripts": { "prepack": "npm run build && npm run format", From b35afe0d9d4fdfc75ced40d945ebec19e5194166 Mon Sep 17 00:00:00 2001 From: Titus Date: Wed, 9 Aug 2023 15:30:52 +0200 Subject: [PATCH 14/46] Add useful error on empty presets Empty presets are most likely a mistake by the user. At best, they do nothing. This improves the situation for humans that make mistakes, by throwing a useful error. Closes GH-200. Closes GH-202. Reviewed-by: Christian Murphy Reviewed-by: Remco Haszing Reviewed-by: JounQin --- lib/index.js | 6 ++++++ test/use.js | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index 925c4dfb..27cd4f0d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -197,6 +197,12 @@ function base() { * @returns {void} */ function addPreset(result) { + if (!('plugins' in result) && !('settings' in result)) { + throw new Error( + 'Expected usable value but received an empty preset, which is probably a mistake: presets typically come with `plugins` and sometimes with `settings`, but this has neither' + ) + } + addList(result.plugins) if (result.settings) { diff --git a/test/use.js b/test/use.js index e3fa5913..ffb2968b 100644 --- a/test/use.js +++ b/test/use.js @@ -291,10 +291,13 @@ test('use(preset)', async (t) => { 'should throw on invalid `plugins` (2)' ) - await t.test('should support empty presets', () => { - const processor = unified().use({}).freeze() - assert.equal(processor.attachers.length, 0) - }) + assert.throws( + () => { + unified().use({}).freeze() + }, + /Expected usable value but received an empty preset/, + 'should throw on empty presets' + ) await t.test('should support presets with empty plugins', () => { const processor = unified().use({plugins: []}).freeze() From ffc146c84f1ce189e0237ca6f7b1ae4266c2c79a Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Wed, 9 Aug 2023 06:37:29 -0700 Subject: [PATCH 15/46] Update `typescript` Closes GH-216. Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer --- package.json | 2 +- test/process.js | 8 ++++++++ test/run.js | 12 ++++++++++++ test/use.js | 28 ++++++++++++++++++++-------- tsconfig.json | 3 --- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index b4a84fef..7f51f052 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "remark-preset-wooorm": "^9.0.0", "tsd": "^0.24.0", "type-coverage": "^2.0.0", - "typescript": "^4.0.0", + "typescript": "^5.0.0", "xo": "^0.54.0" }, "scripts": { diff --git a/test/process.js b/test/process.js index af61ef2f..cdf485e6 100644 --- a/test/process.js +++ b/test/process.js @@ -47,6 +47,10 @@ test('process(file, done)', () => { }) .use( () => + /** + * @param {Node} tree + * @param {VFile} file + */ function (tree, file) { assert.equal(tree, givenNode, 'should pass `tree` to transformers') assert.equal(file, givenFile, 'should pass `file` to transformers') @@ -100,6 +104,10 @@ test('process(file)', () => { }) .use( () => + /** + * @param {Node} tree + * @param {VFile} file + */ function (tree, file) { assert.equal(tree, givenNode, 'should pass `tree` to transformers') assert.equal(file, givenFile, 'should pass `file` to transformers') diff --git a/test/run.js b/test/run.js index 45f0284d..2a1568a1 100644 --- a/test/run.js +++ b/test/run.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('unist').Node} Node + */ + import process from 'node:process' import assert from 'node:assert/strict' import test from 'node:test' @@ -549,6 +553,10 @@ test('runSync(node[, file])', async () => { unified() .use( () => + /** + * @param {Node} tree + * @param {VFile} file + */ function (tree, file) { assert.equal(tree, givenNode, 'passes given tree to transformers') assert.equal(file, givenFile, 'passes given file to transformers') @@ -559,6 +567,10 @@ test('runSync(node[, file])', async () => { unified() .use( () => + /** + * @param {Node} _ + * @param {VFile} file + */ function (_, file) { assert.equal( file.toString(), diff --git a/test/use.js b/test/use.js index ffb2968b..41c580f3 100644 --- a/test/use.js +++ b/test/use.js @@ -1,3 +1,8 @@ +/** + * @typedef {import('unist').Node} Node + * @typedef {import('vfile').VFile} VFile + */ + import assert from 'node:assert/strict' import test from 'node:test' import {unified} from '../index.js' @@ -252,14 +257,21 @@ test('use(plugin[, options])', async (t) => { const condition = true processor - .use(() => (node, file) => { - assert.equal(node, givenNode, 'should attach a transformer (#1)') - assert.ok('message' in file, 'should attach a transformer (#2)') - - if (condition) { - throw new Error('Alpha bravo charlie') - } - }) + .use( + () => + /** + * @param {Node} node + * @param {VFile} file + */ + function (node, file) { + assert.equal(node, givenNode, 'should attach a transformer (#1)') + assert.ok('message' in file, 'should attach a transformer (#2)') + + if (condition) { + throw new Error('Alpha bravo charlie') + } + } + ) .freeze() assert.throws( diff --git a/tsconfig.json b/tsconfig.json index 38a7e9be..80ce6dd3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,11 +6,8 @@ "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, - "forceConsistentCasingInFileNames": true, "lib": ["es2020"], "module": "node16", - "newLine": "lf", - "skipLibCheck": true, "strict": true, "target": "es2020" } From 3f56f0f25ed5fcfbd99cfaa690366584aec132f2 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 15:35:29 +0200 Subject: [PATCH 16/46] Update dev-dependencies --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7f51f052..b4edbac8 100644 --- a/package.json +++ b/package.json @@ -56,20 +56,20 @@ }, "devDependencies": { "@types/extend": "^3.0.0", - "@types/node": "^18.0.0", - "c8": "^7.0.0", - "prettier": "^2.0.0", + "@types/node": "^20.0.0", + "c8": "^8.0.0", + "prettier": "^3.0.0", "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", - "tsd": "^0.24.0", + "tsd": "^0.28.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "xo": "^0.54.0" + "xo": "^0.55.0" }, "scripts": { "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && type-coverage", - "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", + "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", "test-api": "node --conditions development test/index.js", "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" From 250d3e511aa0b9967e79bc59152f340b7971dc8d Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 15:50:06 +0200 Subject: [PATCH 17/46] Remove unneeded explicit types in tests Maybe updating `types/unist` will solve it? Related-to: unifiedjs/unified#216. --- package.json | 4 ++++ test/process.js | 9 --------- test/run.js | 12 ------------ test/use.js | 9 --------- 4 files changed, 4 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index b4edbac8..38fb6cbd 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,10 @@ "atLeast": 100, "detail": true, "strict": true, + "#": "`type-coverage` currently barfs on inferring nodes in plugins, while TS gets it", + "ignoreFiles": [ + "test/**/*" + ], "ignoreCatch": true } } diff --git a/test/process.js b/test/process.js index cdf485e6..f36ff810 100644 --- a/test/process.js +++ b/test/process.js @@ -1,6 +1,5 @@ /** * @typedef {import('unist').Literal} Literal - * @typedef {import('unist').Node} Node * @typedef {import('../index.js').Parser} Parser * @typedef {import('../index.js').Compiler} Compiler */ @@ -47,10 +46,6 @@ test('process(file, done)', () => { }) .use( () => - /** - * @param {Node} tree - * @param {VFile} file - */ function (tree, file) { assert.equal(tree, givenNode, 'should pass `tree` to transformers') assert.equal(file, givenFile, 'should pass `file` to transformers') @@ -104,10 +99,6 @@ test('process(file)', () => { }) .use( () => - /** - * @param {Node} tree - * @param {VFile} file - */ function (tree, file) { assert.equal(tree, givenNode, 'should pass `tree` to transformers') assert.equal(file, givenFile, 'should pass `file` to transformers') diff --git a/test/run.js b/test/run.js index 2a1568a1..45f0284d 100644 --- a/test/run.js +++ b/test/run.js @@ -1,7 +1,3 @@ -/** - * @typedef {import('unist').Node} Node - */ - import process from 'node:process' import assert from 'node:assert/strict' import test from 'node:test' @@ -553,10 +549,6 @@ test('runSync(node[, file])', async () => { unified() .use( () => - /** - * @param {Node} tree - * @param {VFile} file - */ function (tree, file) { assert.equal(tree, givenNode, 'passes given tree to transformers') assert.equal(file, givenFile, 'passes given file to transformers') @@ -567,10 +559,6 @@ test('runSync(node[, file])', async () => { unified() .use( () => - /** - * @param {Node} _ - * @param {VFile} file - */ function (_, file) { assert.equal( file.toString(), diff --git a/test/use.js b/test/use.js index 41c580f3..40429b3a 100644 --- a/test/use.js +++ b/test/use.js @@ -1,8 +1,3 @@ -/** - * @typedef {import('unist').Node} Node - * @typedef {import('vfile').VFile} VFile - */ - import assert from 'node:assert/strict' import test from 'node:test' import {unified} from '../index.js' @@ -259,10 +254,6 @@ test('use(plugin[, options])', async (t) => { processor .use( () => - /** - * @param {Node} node - * @param {VFile} file - */ function (node, file) { assert.equal(node, givenNode, 'should attach a transformer (#1)') assert.ok('message' in file, 'should attach a transformer (#2)') From dd9834a0e18d307574d6c86acb252c3403108386 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 15:51:15 +0200 Subject: [PATCH 18/46] Update `@types/unist` --- package.json | 2 +- test/process.js | 2 +- test/util/simple.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 38fb6cbd..4d603b98 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "index.js" ], "dependencies": { - "@types/unist": "^2.0.0", + "@types/unist": "^3.0.0", "bail": "^2.0.0", "extend": "^3.0.0", "is-buffer": "^2.0.0", diff --git a/test/process.js b/test/process.js index f36ff810..bfba4f5a 100644 --- a/test/process.js +++ b/test/process.js @@ -1,5 +1,5 @@ /** - * @typedef {import('unist').Literal} Literal + * @typedef {import('unist').Literal} Literal * @typedef {import('../index.js').Parser} Parser * @typedef {import('../index.js').Compiler} Compiler */ diff --git a/test/util/simple.js b/test/util/simple.js index 0d0cf1a7..44c8adfe 100644 --- a/test/util/simple.js +++ b/test/util/simple.js @@ -1,5 +1,5 @@ /** - * @typedef {import('unist').Literal} Literal + * @typedef {import('unist').Literal} Literal * @typedef {import('../../index.js').Parser} Parser * @typedef {import('../../index.js').Compiler} Compiler */ From 620ccf99ff11d210a2ea945cd11c43c3b33343ac Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 15:51:46 +0200 Subject: [PATCH 19/46] Update `vfile` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d603b98..71c37920 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", - "vfile": "^5.0.0" + "vfile": "^6.0.0" }, "devDependencies": { "@types/extend": "^3.0.0", From 932c1406766769e4152fb74b81afe7ffd3e15363 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 15:57:01 +0200 Subject: [PATCH 20/46] Change to use `exports` Closes GH-217. Co-authored-by: Remco Haszing --- package.json | 3 +-- test/async-function.js | 4 ++-- test/core.js | 2 +- test/data.js | 2 +- test/freeze.js | 6 +++--- test/parse.js | 2 +- test/process.js | 6 +++--- test/run.js | 8 ++++---- test/stringify.js | 2 +- test/use.js | 2 +- test/util/simple.js | 4 ++-- 11 files changed, 20 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 71c37920..3cf35f4c 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,7 @@ ], "sideEffects": false, "type": "module", - "main": "index.js", - "types": "index.d.ts", + "exports": "./index.js", "files": [ "lib/", "index.d.ts", diff --git a/test/async-function.js b/test/async-function.js index 410c65bf..bec3d901 100644 --- a/test/async-function.js +++ b/test/async-function.js @@ -5,7 +5,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import {VFile} from 'vfile' -import {unified} from '../index.js' +import {unified} from 'unified' test('async function transformer () {}', () => { const givenFile = new VFile('alpha') @@ -16,7 +16,7 @@ test('async function transformer () {}', () => { .use(() => async function () {}) .use( // Note: TS JS doesn’t understand the `Promise` w/o explicit type. - /** @type {import('../index.js').Plugin<[]>} */ + /** @type {import('unified').Plugin<[]>} */ () => async function () { return undefined diff --git a/test/core.js b/test/core.js index caa278eb..1a378cf7 100644 --- a/test/core.js +++ b/test/core.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import test from 'node:test' -import {unified} from '../index.js' +import {unified} from 'unified' test('unified()', () => { /** @type {number} */ diff --git a/test/data.js b/test/data.js index 735c2877..54a6dc3c 100644 --- a/test/data.js +++ b/test/data.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import test from 'node:test' -import {unified} from '../index.js' +import {unified} from 'unified' test('data(key[, value])', () => { const processor = unified() diff --git a/test/freeze.js b/test/freeze.js index dca504bc..b00f4932 100644 --- a/test/freeze.js +++ b/test/freeze.js @@ -1,10 +1,10 @@ /** - * @typedef {import('../index.js').Plugin} Plugin + * @typedef {import('unified').Plugin} Plugin */ import assert from 'node:assert/strict' import test from 'node:test' -import {unified} from '../index.js' +import {unified} from 'unified' import {SimpleCompiler, SimpleParser} from './util/simple.js' test('freeze()', async (t) => { @@ -78,7 +78,7 @@ test('freeze()', async (t) => { processor().freeze() /** - * @this {import('../index.js').Processor} + * @this {import('unified').Processor} * @type {Plugin} */ function freezingPlugin() { diff --git a/test/parse.js b/test/parse.js index ec00bbd9..eff1326d 100644 --- a/test/parse.js +++ b/test/parse.js @@ -5,7 +5,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import {unified} from '../index.js' +import {unified} from 'unified' test('parse(file)', () => { const processor = unified() diff --git a/test/process.js b/test/process.js index bfba4f5a..72dc56ce 100644 --- a/test/process.js +++ b/test/process.js @@ -1,14 +1,14 @@ /** * @typedef {import('unist').Literal} Literal - * @typedef {import('../index.js').Parser} Parser - * @typedef {import('../index.js').Compiler} Compiler + * @typedef {import('unified').Parser} Parser + * @typedef {import('unified').Compiler} Compiler */ import {Buffer} from 'node:buffer' import assert from 'node:assert/strict' import test from 'node:test' import {VFile} from 'vfile' -import {unified} from '../index.js' +import {unified} from 'unified' import {SimpleCompiler, SimpleParser} from './util/simple.js' test('process(file, done)', () => { diff --git a/test/run.js b/test/run.js index 45f0284d..df77a08c 100644 --- a/test/run.js +++ b/test/run.js @@ -2,7 +2,7 @@ import process from 'node:process' import assert from 'node:assert/strict' import test from 'node:test' import {VFile} from 'vfile' -import {unified} from '../index.js' +import {unified} from 'unified' test('run(node[, file], done)', async () => { const givenFile = new VFile('alpha') @@ -144,7 +144,7 @@ test('run(node[, file], done)', async () => { unified() .use( // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('../index.js').Plugin<[]>} */ + /** @type {import('unified').Plugin<[]>} */ () => function () { return new Promise((resolve) => { @@ -436,7 +436,7 @@ test('run(node[, file])', async () => { unified() .use( // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('../index.js').Plugin<[]>} */ + /** @type {import('unified').Plugin<[]>} */ () => function () { return new Promise((resolve) => { @@ -661,7 +661,7 @@ test('runSync(node[, file])', async () => { unified() .use( // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('../index.js').Plugin<[]>} */ + /** @type {import('unified').Plugin<[]>} */ () => function () { return new Promise((resolve) => { diff --git a/test/stringify.js b/test/stringify.js index 9e9ec1c9..b1cb975f 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -5,7 +5,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import {VFile} from 'vfile' -import {unified} from '../index.js' +import {unified} from 'unified' test('stringify(node[, file])', () => { const processor = unified() diff --git a/test/use.js b/test/use.js index 40429b3a..5e672af8 100644 --- a/test/use.js +++ b/test/use.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import test from 'node:test' -import {unified} from '../index.js' +import {unified} from 'unified' test('use(plugin[, options])', async (t) => { await t.test('should ignore missing values', () => { diff --git a/test/util/simple.js b/test/util/simple.js index 44c8adfe..64c12e5f 100644 --- a/test/util/simple.js +++ b/test/util/simple.js @@ -1,7 +1,7 @@ /** * @typedef {import('unist').Literal} Literal - * @typedef {import('../../index.js').Parser} Parser - * @typedef {import('../../index.js').Compiler} Compiler + * @typedef {import('unified').Parser} Parser + * @typedef {import('unified').Compiler} Compiler */ /** @type {Parser} */ From 6edcd14a85190d95e1b80648daa189a09f4a16cf Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 15:58:36 +0200 Subject: [PATCH 21/46] Replace dependency --- lib/index.js | 6 +++--- package.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index 27cd4f0d..e88a604c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,9 +14,9 @@ * @typedef {import('../index.js').ProcessCallback} ProcessCallback */ +import structuredClone from '@ungap/structured-clone' import {bail} from 'bail' import isBuffer from 'is-buffer' -import extend from 'extend' import isPlainObj from 'is-plain-obj' import {trough} from 'trough' import {VFile} from 'vfile' @@ -77,7 +77,7 @@ function base() { destination.use(...attachers[index]) } - destination.data(extend(true, {}, namespace)) + destination.data(structuredClone(namespace)) return destination } @@ -248,7 +248,7 @@ function base() { if (entry) { if (isPlainObj(entry[1]) && isPlainObj(value)) { - value = extend(true, entry[1], value) + value = structuredClone({...entry[1], ...value}) } entry[1] = value diff --git a/package.json b/package.json index 3cf35f4c..68e6c383 100644 --- a/package.json +++ b/package.json @@ -46,16 +46,16 @@ ], "dependencies": { "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", "bail": "^2.0.0", - "extend": "^3.0.0", "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" }, "devDependencies": { - "@types/extend": "^3.0.0", "@types/node": "^20.0.0", + "@types/ungap__structured-clone": "^0.3.0", "c8": "^8.0.0", "prettier": "^3.0.0", "remark-cli": "^11.0.0", From 294834efd9b155556b9157bd82463c6e7d8715c1 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 16:05:05 +0200 Subject: [PATCH 22/46] Update Actions --- .github/workflows/main.yml | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 31dd3214..c577ef57 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: - push jobs: main: - name: test / unified / ${{matrix.node}} on ${{matrix.os}} + name: ${{matrix.node}} on ${{matrix.os}} runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v3 @@ -20,42 +20,27 @@ jobs: - ubuntu-latest - windows-latest node: - - lts/hydrogen + - lts/gallium - node canary: - name: canary / ${{matrix.package}} / ${{matrix.node}} on ${{matrix.os}} - runs-on: ${{matrix.os}} + name: canary / ${{matrix.package}} + runs-on: ubuntu-latest steps: - - name: checkout unified - uses: actions/checkout@v3 - - name: setup node - uses: actions/setup-node@v3 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: ${{matrix.node}} - - name: setup unified - run: | - npm install -g npm - npm install - npm run build - - name: checkout ${{matrix.package}} - uses: actions/checkout@v3 + node-version: node + - run: npm install + - run: npm run build + - uses: actions/checkout@v3 with: repository: ${{matrix.package}} path: canary/${{matrix.package}} - - name: test ${{matrix.package}} - run: | - npm install - npx rimraf "node_modules/**/unified" - npm test + - run: npm install && npx rimraf "node_modules/**/unified" && npm test working-directory: canary/${{matrix.package}} strategy: fail-fast: false - max-parallel: 2 matrix: - os: - - ubuntu-latest - node: - - lts/hydrogen package: - unifiedjs/unified-engine - remarkjs/remark From 904f4c1cc73c0734d45046bd89ceba541452c5f5 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 16:14:34 +0200 Subject: [PATCH 23/46] Refactor `.gitignore` --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fee8160d..fcb26070 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ coverage/ node_modules/ .DS_Store -lib/**/*.d.ts -test/**/*.d.ts +*.d.ts *.log yarn.lock +!/index.d.ts From d6f37dd44f8f5b689068bed60caf101a3bedd92b Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 16:14:39 +0200 Subject: [PATCH 24/46] Add `ignore-scripts` to `.npmrc` --- .npmrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmrc b/.npmrc index 43c97e71..3757b304 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ +ignore-scripts=true package-lock=false From 49196e0f1ad598d90f5a4a1f50ad72e5eeee4e65 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 16:14:57 +0200 Subject: [PATCH 25/46] Refactor to reorder canaries --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c577ef57..64bf7365 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,7 @@ jobs: fail-fast: false matrix: package: - - unifiedjs/unified-engine - - remarkjs/remark - rehypejs/rehype + - remarkjs/remark - retextjs/retext + - unifiedjs/unified-engine From b185e2db8993229fa67bd0d191303d2ef5300fe0 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 16:26:05 +0200 Subject: [PATCH 26/46] Refactor `package.json` --- package.json | 71 +++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 68e6c383..5b45b7ba 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,24 @@ { "name": "unified", "version": "10.1.2", - "description": "Interface for parsing, inspecting, transforming, and serializing content through syntax trees", + "description": "parse, inspect, transform, and serialize content through syntax trees", "license": "MIT", "keywords": [ - "unified", - "process", - "parse", - "transform", - "compile", - "stringify", - "serialize", "ast", - "cst", - "syntax", - "tree", + "compile", "content", + "cst", + "parse", + "process", "rehype", + "remark", "retext", - "remark" + "serialize", + "stringify", + "syntax", + "transform", + "tree", + "unified" ], "homepage": "https://unifiedjs.com", "repository": "unifiedjs/unified", @@ -66,34 +66,24 @@ "xo": "^0.55.0" }, "scripts": { - "prepack": "npm run build && npm run format", "build": "tsc --build --clean && tsc --build && type-coverage", - "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", + "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", + "prepack": "npm run build && npm run format", + "test": "npm run build && npm run format && npm run test-coverage", "test-api": "node --conditions development test/index.js", - "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", - "test": "npm run build && npm run format && npm run test-coverage" + "test-coverage": "c8 --100 --check-coverage --reporter lcov npm run test-api" }, "prettier": { - "tabWidth": 2, - "useTabs": false, - "singleQuote": true, "bracketSpacing": false, + "singleQuote": true, "semi": false, - "trailingComma": "none" - }, - "xo": { - "prettier": true, - "rules": { - "@typescript-eslint/ban-types": "off", - "promise/param-names": "off" - }, - "ignores": [ - "types/" - ] + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false }, "remarkConfig": { "plugins": [ - "preset-wooorm", + "remark-preset-wooorm", [ "remark-lint-no-html", false @@ -103,11 +93,24 @@ "typeCoverage": { "atLeast": 100, "detail": true, - "strict": true, + "ignoreCatch": true, "#": "`type-coverage` currently barfs on inferring nodes in plugins, while TS gets it", "ignoreFiles": [ - "test/**/*" + "test/**/*.js" + ], + "strict": true + }, + "xo": { + "overrides": [ + { + "files": [ + "**/*.ts" + ], + "rules": { + "@typescript-eslint/ban-types": "off" + } + } ], - "ignoreCatch": true + "prettier": true } } From 6ff42908cf3397ccea6ce602495cb343b41b17fc Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 9 Aug 2023 16:27:04 +0200 Subject: [PATCH 27/46] Refactor `tsconfig.json` --- tsconfig.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 80ce6dd3..ad1496e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,7 @@ { - "include": ["**/**.js", "index.d.ts"], - "exclude": ["coverage", "node_modules"], "compilerOptions": { "checkJs": true, + "customConditions": ["development"], "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, @@ -10,5 +9,7 @@ "module": "node16", "strict": true, "target": "es2020" - } + }, + "exclude": ["coverage/", "node_modules/"], + "include": ["**/*.js", "index.d.ts"] } From ad06700216d5b02a2d00c13b85900228c2b36eff Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 10 Aug 2023 16:38:01 +0200 Subject: [PATCH 28/46] Refactor code-style --- index.d.ts | 15 +- index.test-d.ts | 846 ++++++++++++++--------- lib/index.js | 62 +- package.json | 5 +- readme.md | 2 +- test/async-function.js | 35 - test/core.js | 52 +- test/data.js | 69 +- test/freeze.js | 278 ++++++-- test/index.js | 10 +- test/parse.js | 130 ++-- test/process-compilers.js | 73 ++ test/process-sync.js | 47 ++ test/process.js | 293 ++------ test/run-sync.js | 188 +++++ test/run.js | 1130 ++++++++++++++---------------- test/stringify.js | 131 ++-- test/use.js | 1376 ++++++++++++++++++++++++++++++------- test/util/simple.js | 5 +- 19 files changed, 2979 insertions(+), 1768 deletions(-) delete mode 100644 test/async-function.js create mode 100644 test/process-compilers.js create mode 100644 test/process-sync.js create mode 100644 test/run-sync.js diff --git a/index.d.ts b/index.d.ts index 06762582..6f881a87 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,16 +16,16 @@ import type {Node} from 'unist' import type {VFile, VFileCompatible} from 'vfile' -/* eslint-disable @typescript-eslint/naming-convention */ - -type VFileWithOutput = Result extends Uint8Array // Buffer. +type VFileWithOutput = Result extends Uint8Array ? VFile : Result extends object // Custom result type ? VFile & {result: Result} : VFile // Get the right most non-void thing. -type Specific = Right extends void ? Left : Right +type Specific = Right extends undefined | void + ? Left + : Right // Create a processor based on the input/output of a plugin. type UsePlugin< @@ -71,8 +71,6 @@ type UsePlugin< // just keep it as it was. Processor -/* eslint-enable @typescript-eslint/naming-convention */ - /** * Processor allows plugins to be chained together to transform content. * The chain of plugins defines how content flows through it. @@ -203,7 +201,7 @@ export type Processor< * Current processor. */ use( - presetOrList: Preset | PluggableList + presetOrList: PluggableList | Preset ): Processor } & FrozenProcessor @@ -238,6 +236,7 @@ export type FrozenProcessor< attachers: Array<[Plugin, ...unknown[]]> Parser?: Parser> | undefined + Compiler?: | Compiler, Specific> | undefined @@ -656,7 +655,7 @@ export type Transformer< node: Input, file: VFile, next: TransformCallback -) => Promise | Output | Error | undefined | void +) => Promise | Error | Output | undefined | void /** * Callback you must call when a transformer is done. diff --git a/index.test-d.ts b/index.test-d.ts index 875e976f..9d71acb0 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,11 +1,12 @@ -import type {Buffer} from 'node:buffer' -import {expectType, expectError} from 'tsd' -import type {Node, Parent, Literal} from 'unist' +import type {Root as HastRoot} from 'hast' +import type {Root as MdastRoot} from 'mdast' +import {expectType} from 'tsd' +import type {Node as UnistNode} from 'unist' import type {VFile} from 'vfile' import type { + FrozenProcessor, Plugin, Processor, - FrozenProcessor, TransformCallback } from './index.js' import {unified} from './index.js' @@ -13,60 +14,177 @@ import {unified} from './index.js' expectType(unified()) expectType(unified().freeze()) -type ExamplePluginSettings = { +type ReactNode = { + kind: string +} + +type ExampleOptionalOptions = { + example?: string | null | undefined +} + +type ExampleRequiredOptions = { example: string } -const pluginWithoutOptions: Plugin = function (options) { - expectType(options) +const hastRoot: HastRoot = { + type: 'root', + children: [{type: 'element', tagName: 'p', properties: {}, children: []}] +} + +const mdastRoot: MdastRoot = { + type: 'root', + children: [{type: 'paragraph', children: []}] } -// Explicitly typed plugins. +// # Explicitly typed plugins + +// ## Plugin w/o options +const pluginWithoutOptions: Plugin<[]> = function () { + // Empty. +} unified().use(pluginWithoutOptions) -expectError(unified().use(pluginWithoutOptions, {})) -expectError(unified().use(pluginWithoutOptions, '')) +unified().use( + pluginWithoutOptions, + // @ts-expect-error: plugin does not expect options. + {} +) +unified().use( + pluginWithoutOptions, + // @ts-expect-error: plugin does not expect `string` as options. + '' +) +unified().use( + pluginWithoutOptions, + // @ts-expect-error: plugin does not expect anything. + undefined +) -const pluginWithOptionalOptions: Plugin<[ExamplePluginSettings?]> = function ( - options -) { - expectType(options) +// ## Plugin w/ optional options +const pluginWithOptionalOptions: Plugin< + [(ExampleOptionalOptions | null | undefined)?] +> = function (options) { + expectType(options) } unified().use(pluginWithOptionalOptions) -expectError(unified().use(pluginWithOptionalOptions, {})) -unified().use(pluginWithOptionalOptions, {example: ''}) +unified().use(pluginWithOptionalOptions, {}) +unified().use(pluginWithOptionalOptions, {example: null}) +unified().use(pluginWithOptionalOptions, {example: undefined}) +unified().use(pluginWithOptionalOptions, {example: 'asd'}) +unified().use( + pluginWithOptionalOptions, + // @ts-expect-error: plugin does not accept `whatever`. + {whatever: 1} +) -const pluginWithOptions: Plugin<[ExamplePluginSettings]> = function (options) { - expectType(options) +// ## Plugin w/ required options +const pluginWithOptions: Plugin<[ExampleRequiredOptions]> = function (options) { + expectType(options) } -expectError(unified().use(pluginWithOptions)) -expectError(unified().use(pluginWithOptions, {})) +// @ts-expect-error: plugin requires options. +unified().use(pluginWithOptions) +unified().use( + pluginWithOptions, + // @ts-expect-error: plugin requires particular option. + {} +) unified().use(pluginWithOptions, {example: ''}) -const pluginWithSeveralOptions: Plugin<[ExamplePluginSettings, number]> = +// ## Plugin w/ several arguments +const pluginWithSeveralArguments: Plugin<[ExampleRequiredOptions, number]> = function (options, value) { - expectType(options) + expectType(options) expectType(value) } -expectError(unified().use(pluginWithSeveralOptions)) -expectError(unified().use(pluginWithSeveralOptions, {})) -expectError(unified().use(pluginWithSeveralOptions, {example: ''})) -unified().use(pluginWithSeveralOptions, {example: ''}, 1) +// @ts-expect-error: plugin requires options. +unified().use(pluginWithSeveralArguments) +unified().use( + pluginWithSeveralArguments, + // @ts-expect-error: plugin requires particular option. + {} +) +unified().use( + pluginWithSeveralArguments, + // @ts-expect-error: plugin requires more arguments. + {example: ''} +) +unified().use(pluginWithSeveralArguments, {example: ''}, 1) -// Implicitly typed plugins. +// # Implicitly typed plugins. -const pluginWithImplicitOptions = (options?: ExamplePluginSettings) => { - expectType(options) +// ## Plugin without options. + +function pluginWithoutOptionsImplicit() { + // Empty. } -unified().use(pluginWithImplicitOptions) -expectError(unified().use(pluginWithImplicitOptions, {})) -unified().use(pluginWithImplicitOptions, {example: ''}) +unified().use(pluginWithoutOptionsImplicit) +unified().use( + pluginWithoutOptionsImplicit, + // @ts-expect-error: plugin does not accept options. + {} +) + +// ## Plugin w/ optional options + +function pluginWithOptionalOptionsImplicit( + options?: ExampleOptionalOptions | null | undefined +) { + expectType(options) +} -// Using many different forms to pass options. +unified().use(pluginWithOptionalOptionsImplicit) +unified().use(pluginWithOptionalOptionsImplicit, {}) +unified().use(pluginWithOptionalOptionsImplicit, {example: null}) +unified().use(pluginWithOptionalOptionsImplicit, {example: undefined}) +unified().use(pluginWithOptionalOptionsImplicit, {example: 'asd'}) +unified().use( + pluginWithOptionalOptionsImplicit, + // @ts-expect-error: plugin does not accept `whatever`. + {whatever: 1} +) + +// ## Plugin w/ required options +function pluginWithOptionsImplicit(options: ExampleRequiredOptions) { + expectType(options) +} + +// @ts-expect-error: plugin requires options. +unified().use(pluginWithOptionsImplicit) +unified().use( + pluginWithOptionsImplicit, + // @ts-expect-error: plugin requires particular option. + {} +) +unified().use(pluginWithOptionsImplicit, {example: ''}) + +// ## Plugin w/ several arguments +function pluginWithSeveralArgumentsImplicit( + options: ExampleRequiredOptions, + value: number +) { + expectType(options) + expectType(value) +} + +// @ts-expect-error: plugin requires options. +unified().use(pluginWithSeveralArgumentsImplicit) +unified().use( + pluginWithSeveralArgumentsImplicit, + // @ts-expect-error: plugin requires particular option. + {} +) +unified().use( + pluginWithSeveralArgumentsImplicit, + // @ts-expect-error: plugin requires more arguments. + {example: ''} +) +unified().use(pluginWithSeveralArgumentsImplicit, {example: ''}, 1) + +// # Different ways of passing options unified() .use(pluginWithOptions, {example: ''}) @@ -76,7 +194,7 @@ unified() plugins: [[pluginWithOptions, {example: ''}]] }) -// Using booleans to turn on or off plugins. +// # Turning plugins on/off w/ booleans unified() .use(pluginWithoutOptions, true) @@ -94,376 +212,454 @@ unified() ] }) -// Plugins setting parsers/compilers +// # Plugin defining parser/compiler unified().use(function () { // Function. - this.Parser = (doc, file) => { + this.Parser = function (doc, file) { expectType(doc) expectType(file) return {type: ''} } // Class. - this.Parser = class P { - constructor(doc: string, file: VFile) { - // Looks useless but ensures this class is assignable - expectType(doc) - expectType(file) - } - + this.Parser = class { parse() { return {type: 'x'} } } // Function. - this.Compiler = (tree, file) => { - expectType(tree) + this.Compiler = function (tree, file) { + expectType(tree) expectType(file) return '' } - this.Compiler = class C { - constructor(node: Node, file: VFile) { - // Looks useless but ensures this class is assignable - expectType(node) - expectType(file) - } - + this.Compiler = class { compile() { return '' } } }) -// Plugins returning a transformer. +// # Plugins w/ transformer unified() - .use(() => (tree, file, next) => { - expectType(tree) - expectType(file) - expectType(next) - setImmediate(next) + // Sync w/ nothing (baseline). + .use(function () { + return function (tree, file) { + expectType(tree) + expectType(file) + } }) - .use(() => async (tree, file) => { - expectType(tree) - expectType(file) - return {type: 'x'} + // Sync yielding tree. + .use(function () { + return function () { + return {type: 'x'} + } }) - .use(() => () => ({type: 'x'})) - .use(() => () => undefined) - .use(() => () => { - /* Empty */ + // Sync yielding explicit `undefined`. + .use(function () { + return function () { + return undefined + } }) - .use(() => (x) => { - if (x) { - throw new Error('x') + // Sync yielding implicit void. + .use(function () { + return function () { + // Empty. + } + }) + // Sync yielding error. + .use(function () { + return function (x) { + return new Error('x') + } + }) + // Sync throwing error. + .use(function () { + return function (x) { + // To do: investigate if we can support `never` by dropping this useless condition. + if (x) { + throw new Error('x') + } } }) -// Plugins bound to a certain node. - -// A small subset of mdast. -type MdastRoot = { - type: 'root' - children: MdastFlow[] -} & Parent - -type MdastFlow = MdastParagraph - -type MdastParagraph = { - type: 'paragraph' - children: MdastPhrasing[] -} & Parent - -type MdastPhrasing = MdastText - -type MdastText = { - type: 'text' - value: string -} & Literal + // Sync calling `next` w/ tree. + .use(function () { + return function (_1, _2, next) { + expectType(next) + next(undefined, {type: 'x'}) + } + }) + // Sync calling `next` w/ error. + .use(function () { + return function (_1, _2, next) { + next(new Error('x')) + } + }) + // Async calling `next`. + .use(function () { + return function (_1, _2, next) { + setImmediate(function () { + next() + }) + } + }) + // Async calling `next` w/ tree. + .use(function () { + return function (_1, _2, next) { + setImmediate(function () { + next(undefined, {type: 'x'}) + }) + } + }) + // Async calling `next` w/ error. + .use(function () { + return function (_1, _2, next) { + setImmediate(function () { + next(new Error('x')) + }) + } + }) -// A small subset of hast. -type HastRoot = { - type: 'root' - children: HastChild[] -} & Parent + // Resolving nothing (baseline). + .use(function () { + return async function (tree, file) { + expectType(tree) + expectType(file) + } + }) + // Resolving tree. + .use(function () { + return async function () { + return {type: 'x'} + } + }) + // To do: investigate why TS barfs on `Promise`? + // // Resolving explicit `undefined`. + // .use(function () { + // return async function () { + // return undefined + // } + // }) + // Resolving implicit void. + .use(function () { + return async function () { + // Empty. + } + }) + // Rejecting error. + .use(function () { + return async function (x) { + // To do: investigate if we can support `never` by dropping this useless condition. + if (x) { + throw new Error('x') + } + } + }) -type HastChild = HastElement | HastText +// # Plugins bound to a certain node -type HastElement = { - type: 'element' - tagName: string - properties: Record - children: HastChild[] -} & Parent +// Parse plugins. +const remarkParse: Plugin<[], string, MdastRoot> = function () { + // Empty. +} -type HastText = { - type: 'text' - value: string -} & Literal +const processorWithRemarkParse = unified() + .use(remarkParse) + .use(function () { + return function (tree) { + expectType(tree) + } + }) -const explicitPluginWithInputTree: Plugin = - () => (tree, file) => { - expectType(tree) - expectType(file) - } +expectType>(processorWithRemarkParse) +expectType(processorWithRemarkParse.parse('')) +// To do: accept `UnistNode`? +expectType(processorWithRemarkParse.runSync(mdastRoot)) +// @ts-expect-error: to do: accept `UnistNode`? +expectType(processorWithRemarkParse.runSync(hastRoot)) +// To do: yield `never`, accept `UnistNode`? +expectType(processorWithRemarkParse.stringify(mdastRoot)) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRemarkParse.stringify(hastRoot) +expectType(processorWithRemarkParse.processSync('')) + +// Inspect/transform plugin (explicit). +const remarkLint: Plugin<[], MdastRoot> = function () { + // Empty. +} -const explicitPluginWithTrees: Plugin = - () => (tree, file) => { - expectType(tree) - expectType(file) - return { - type: 'root', - children: [ - { - type: 'element', - tagName: 'a', - properties: {}, - children: [{type: 'text', value: 'a'}] - } - ] +const processorWithRemarkLint = unified() + .use(remarkLint) + .use(function () { + return function (tree) { + expectType(tree) } - } + }) -unified().use(explicitPluginWithInputTree) -unified().use([explicitPluginWithInputTree]) -unified().use({plugins: [explicitPluginWithInputTree], settings: {}}) -unified().use(() => (tree: MdastRoot) => { - expectType(tree) -}) -unified().use([ - () => (tree: MdastRoot) => { +// To do: `UnistNode`, `MdastRoot`, `UnistNode`? +expectType>(processorWithRemarkLint) +// To do: yield `UnistNode`? +expectType(processorWithRemarkLint.parse('')) +expectType(processorWithRemarkLint.runSync(mdastRoot)) +// @ts-expect-error: not the correct node type. +expectType(processorWithRemarkLint.runSync(hastRoot)) +// To do: yield `never`, accept `UnistNode`? +expectType(processorWithRemarkLint.stringify(mdastRoot)) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRemarkLint.stringify(hastRoot) +expectType(processorWithRemarkLint.processSync('')) + +// Inspect/transform plugin (implicit). +function remarkLintImplicit() { + return function (tree: MdastRoot) { expectType(tree) + return mdastRoot } -]) -unified().use({ - plugins: [ - () => (tree: MdastRoot) => { +} + +const processorWithRemarkLintImplicit = unified() + .use(remarkLintImplicit) + .use(function () { + return function (tree) { expectType(tree) } - ], - settings: {} -}) - -unified().use(explicitPluginWithTrees) -unified().use([explicitPluginWithTrees]) -unified().use({plugins: [explicitPluginWithTrees], settings: {}}) -unified().use(() => (_: MdastRoot) => ({ - type: 'root', - children: [{type: 'text', value: 'a'}] -})) -unified().use([ - () => (_: MdastRoot) => ({ - type: 'root', - children: [{type: 'text', value: 'a'}] }) -]) -unified().use({ - plugins: [ - () => (_: MdastRoot) => ({ - type: 'root', - children: [{type: 'text', value: 'a'}] - }) - ], - settings: {} -}) -// Input and output types. -type ReactNode = { - kind: string +// To do: `UnistNode`, `MdastRoot`, `UnistNode`? +expectType>( + processorWithRemarkLintImplicit +) +// To do: yield `UnistNode`? +expectType(processorWithRemarkLintImplicit.parse('')) +expectType(processorWithRemarkLintImplicit.runSync(mdastRoot)) +// @ts-expect-error: not the correct node type. +processorWithRemarkLintImplicit.runSync(hastRoot) +// To do: yield `never`, accept `UnistNode`? +expectType(processorWithRemarkLintImplicit.stringify(mdastRoot)) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRemarkLintImplicit.stringify(hastRoot) +expectType(processorWithRemarkLintImplicit.processSync('')) + +// Mutate plugin (explicit). +const remarkRehype: Plugin<[], MdastRoot, HastRoot> = function () { + // Empty. } -const someMdast: MdastRoot = { - type: 'root', - children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}] +const processorWithRemarkRehype = unified() + .use(remarkRehype) + .use(function () { + return function (tree) { + expectType(tree) + } + }) + +// To do: `UnistNode`, `MdastRoot`, `UnistNode`? +expectType>(processorWithRemarkRehype) +// To do: yield `UnistNode`? +expectType(processorWithRemarkRehype.parse('')) +expectType(processorWithRemarkRehype.runSync(mdastRoot)) +// @ts-expect-error: not the correct node type. +processorWithRemarkRehype.runSync(hastRoot) +// To do: yield `never`? +expectType(processorWithRemarkRehype.stringify(hastRoot)) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRemarkRehype.stringify(mdastRoot) +expectType(processorWithRemarkRehype.processSync('')) + +// Mutate plugin (implicit). +function remarkRehypeImplicit() { + return function (tree: MdastRoot) { + expectType(tree) + return hastRoot + } } -const someHast: HastRoot = { - type: 'root', - children: [ - { - type: 'element', - tagName: 'a', - properties: {}, - children: [{type: 'text', value: 'a'}] +const processorWithRemarkRehypeImplicit = unified() + .use(remarkRehypeImplicit) + .use(function () { + return function (tree) { + expectType(tree) } - ] -} + }) -const remarkParse: Plugin = () => { - /* Empty */ +// To do: `UnistNode`, `MdastRoot`, `UnistNode`? +expectType>( + processorWithRemarkRehypeImplicit +) +// To do: yield `UnistNode`? +expectType(processorWithRemarkRehypeImplicit.parse('')) +expectType(processorWithRemarkRehypeImplicit.runSync(mdastRoot)) +// @ts-expect-error: not the correct node type. +processorWithRemarkRehypeImplicit.runSync(hastRoot) +// To do: yield `never`? +expectType(processorWithRemarkRehypeImplicit.stringify(hastRoot)) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRemarkRehypeImplicit.stringify(mdastRoot) +expectType(processorWithRemarkRehypeImplicit.processSync('')) + +// Compile plugin. +const rehypeStringify: Plugin<[], HastRoot, string> = function () { + // Empty. } -const remarkStringify: Plugin = () => { - /* Empty */ -} +const processorWithRehypeStringify = unified().use(rehypeStringify) -const rehypeParse: Plugin = () => { - /* Empty */ +// To do: ? +expectType>( + processorWithRehypeStringify +) +// To do: yield `UnistNode`? +expectType(processorWithRehypeStringify.parse('')) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRehypeStringify.runSync(mdastRoot) +// To do: accept, yield `UnistNode`? +expectType(processorWithRehypeStringify.runSync(hastRoot)) +expectType(processorWithRehypeStringify.stringify(hastRoot)) +// @ts-expect-error: not the correct node type. +processorWithRehypeStringify.stringify(mdastRoot) +expectType(processorWithRehypeStringify.processSync('')) + +// Compile plugin (to a buffer). +const rehypeStringifyBuffer: Plugin<[], HastRoot, Uint8Array> = function () { + // Empty. } -const rehypeStringify: Plugin = () => { - /* Empty */ -} +const processorWithRehypeStringifyBuffer = unified().use(rehypeStringifyBuffer) -const rehypeStringifyBuffer: Plugin = () => { - /* Empty */ +// To do: ? +expectType>( + processorWithRehypeStringifyBuffer +) +// To do: yield `UnistNode`? +expectType(processorWithRehypeStringifyBuffer.parse('')) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRehypeStringifyBuffer.runSync(mdastRoot) +// To do: accept, yield `UnistNode`? +expectType(processorWithRehypeStringifyBuffer.runSync(hastRoot)) +expectType(processorWithRehypeStringifyBuffer.stringify(hastRoot)) +// @ts-expect-error: not the correct node type. +processorWithRehypeStringifyBuffer.stringify(mdastRoot) +expectType(processorWithRehypeStringifyBuffer.processSync('')) + +// Compile plugin (to a non-node). +const rehypeReact: Plugin<[], HastRoot, ReactNode> = function () { + // Empty. } -const explicitRemarkPlugin: Plugin = () => { - /* Empty */ -} +const processorWithRehypeReact = unified().use(rehypeReact) -const implicitPlugin: Plugin = () => { - /* Empty */ -} +// To do: ? +expectType>( + processorWithRehypeReact +) +// To do: yield `UnistNode`? +expectType(processorWithRehypeReact.parse('')) +// @ts-expect-error: to do: accept `UnistNode`? +processorWithRehypeReact.runSync(mdastRoot) +// To do: accept, yield `UnistNode`? +expectType(processorWithRehypeReact.runSync(hastRoot)) +expectType(processorWithRehypeReact.stringify(hastRoot)) +// @ts-expect-error: not the correct node type. +processorWithRehypeReact.stringify(mdastRoot) +expectType( + processorWithRehypeReact.processSync('') +) -const remarkRehype: Plugin = () => { - /* Empty */ -} +// All together. +const processorWithAll = unified() + .use(remarkParse) + .use(remarkLint) + .use(remarkLintImplicit) + .use(remarkRehype) + .use(rehypeStringify) -const explicitRehypePlugin: Plugin = () => { - /* Empty */ -} +expectType>(processorWithAll) +expectType(processorWithAll.parse('')) +expectType(processorWithAll.runSync(mdastRoot)) +// @ts-expect-error: not the correct node type. +processorWithAll.runSync(hastRoot) +expectType(processorWithAll.stringify(hastRoot)) +// @ts-expect-error: not the correct node type. +processorWithAll.stringify(mdastRoot) +expectType(processorWithAll.processSync('')) -const rehypeReact: Plugin = () => { - /* Empty */ -} +// # Different ways to use plugins -// If a plugin is defined with string as input and a node as output, it -// configures a parser. -expectType(unified().use(remarkParse).parse('')) -expectType(unified().use(rehypeParse).parse('')) -expectType(unified().parse('')) // No parser. - -// If a plugin is defined with a node as input and a non-node as output, it -// configures a compiler. -expectType(unified().use(remarkStringify).stringify(someMdast)) -expectType(unified().use(rehypeStringify).stringify(someHast)) -expectType(unified().use(rehypeStringifyBuffer).stringify(someHast)) -expectType(unified().stringify(someHast)) // No compiler. -expectType(unified().use(rehypeReact).stringify(someHast)) -expectError(unified().use(remarkStringify).stringify(someHast)) -expectError(unified().use(rehypeStringify).stringify(someMdast)) - -// Compilers configure the output of `process`, too. -expectType(unified().use(remarkStringify).processSync('')) -expectType(unified().use(rehypeStringify).processSync('')) -expectType(unified().use(rehypeStringifyBuffer).processSync('')) -expectType(unified().processSync('')) -expectType( - unified().use(rehypeReact).processSync('') +expectType>( + unified().use([remarkParse]) ) -// A parser plugin defines the input of `.run`: -expectType>(unified().use(remarkParse).run(someMdast)) -expectType(unified().use(remarkParse).runSync(someMdast)) -expectError(unified().use(remarkParse).run(someHast)) -expectError(unified().use(remarkParse).runSync(someHast)) - -// A compiler plugin defines the input/output of `.run`: -expectError(unified().use(rehypeStringify).run(someMdast)) -expectError(unified().use(rehypeStringify).runSync(someMdast)) -// As a parser and a compiler are set, it can be assumed that the input of `run` -// is the result of the parser, and the output is the input of the compiler. -expectType>( - unified().use(remarkParse).use(rehypeStringify).run(someMdast) -) -expectType( - unified().use(remarkParse).use(rehypeStringify).runSync(someMdast) +expectType>( + unified().use([ + remarkParse, + // @ts-expect-error: to do: investigate. + remarkLint, + remarkLintImplicit, + remarkRehype, + rehypeStringify + ]) ) -// Probably hast expected. -expectError(unified().use(rehypeStringify).runSync(someMdast)) -expectType(await unified().use(rehypeStringify).run(someHast)) - -unified() - .use(rehypeStringify) - .run(someHast, (error, thing) => { - expectType(error) - expectType(thing) +expectType>( + // @ts-expect-error: to do: investigate. + unified().use({ + plugins: [remarkParse] }) - -// A compiler plugin defines the output of `.process`: -expectType( - unified().use(rehypeReact).processSync('') -) -expectType( - unified().use(remarkParse).use(rehypeReact).processSync('') ) -expectType( - await unified().use(rehypeReact).process('') +expectType>( + unified().use({ + // @ts-expect-error: to do: investigate. + plugins: [ + remarkParse, + remarkLint, + remarkLintImplicit, + remarkRehype, + rehypeStringify + ] + }) ) -unified() - .use(rehypeReact) - .process('', (error, thing) => { - expectType(error) - expectType<(VFile & {result: ReactNode}) | undefined>(thing) +expectType>( + unified().use({ + // @ts-expect-error: to do: investigate. + plugins: [ + remarkParse, + remarkLint, + remarkLintImplicit, + remarkRehype, + rehypeStringify + ], + settings: {something: 'stuff'} }) +) -// Plugins work! -unified() - .use(remarkParse) - .use(explicitRemarkPlugin) - .use(implicitPlugin) - .use(remarkRehype) - .use(implicitPlugin) - .use(rehypeStringify) - .freeze() +// # Using multiple parsers/compilers -// Parsers define the input of transformers. -unified().use(() => (node) => { - expectType(node) -}) -unified() - .use(remarkParse) - .use(() => (node) => { - expectType(node) - }) -unified() - .use(rehypeParse) - .use(() => (node) => { - expectType(node) - }) +const rehypeParse: Plugin<[], string, HastRoot> = function () { + // Empty. +} -unified() - // Using a parser plugin also defines the current tree (see next). - .use(remarkParse) - // A plugin following a typed parser receives the defined AST. - // If it doesn’t resolve anything, that AST remains for the next plugin. - .use(() => (node) => { - expectType(node) - }) - // A plugin that returns a certain AST, defines it for the next plugin. - .use(() => (node) => { - expectType(node) - return someHast - }) - .use(() => (node) => { - expectType(node) - }) - .use(rehypeStringify) +const remarkStringify: Plugin<[], MdastRoot, string> = function () { + // Empty. +} -// Using two parsers or compilers is fine. The last one sticks. -const p1 = unified().use(remarkParse).use(rehypeParse) -expectType(p1.parse('')) -const p2 = unified().use(remarkStringify).use(rehypeStringify) -expectError(p2.stringify(someMdast)) +expectType(unified().use(remarkParse).use(rehypeParse).parse('')) -// Using mismatched explicit plugins is fine (for now). -unified() - .use(explicitRemarkPlugin) - .use(explicitRehypePlugin) - .use(explicitRemarkPlugin) - -expectType( - unified() - .use(explicitRemarkPlugin) - .use(remarkRehype) - .use(explicitRehypePlugin) - .runSync(someMdast) +expectType( + unified().use(remarkStringify).use(rehypeStringify).stringify(hastRoot) ) + +// # Using mismatched inspect/transform plugins + +const rehypeClassNames: Plugin<[], HastRoot> = function () { + // Empty. +} + +// To do: investigate. +unified().use(remarkLint).use(rehypeClassNames) diff --git a/lib/index.js b/lib/index.js index e88a604c..f02abd1a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,17 +1,19 @@ /** * @typedef {import('unist').Node} Node + * * @typedef {import('vfile').VFileCompatible} VFileCompatible * @typedef {import('vfile').VFileValue} VFileValue - * @typedef {import('../index.js').Processor} Processor - * @typedef {import('../index.js').Plugin} Plugin - * @typedef {import('../index.js').Preset} Preset + * + * @typedef {import('../index.js').Compiler} Compiler + * @typedef {import('../index.js').Parser} Parser * @typedef {import('../index.js').Pluggable} Pluggable * @typedef {import('../index.js').PluggableList} PluggableList - * @typedef {import('../index.js').Transformer} Transformer - * @typedef {import('../index.js').Parser} Parser - * @typedef {import('../index.js').Compiler} Compiler - * @typedef {import('../index.js').RunCallback} RunCallback + * @typedef {import('../index.js').Plugin} Plugin + * @typedef {import('../index.js').Preset} Preset * @typedef {import('../index.js').ProcessCallback} ProcessCallback + * @typedef {import('../index.js').Processor} Processor + * @typedef {import('../index.js').RunCallback} RunCallback + * @typedef {import('../index.js').Transformer} Transformer */ import structuredClone from '@ungap/structured-clone' @@ -36,7 +38,7 @@ function base() { const attachers = [] /** @type {{settings?: Record} & Record} */ let namespace = {} - /** @type {boolean|undefined} */ + /** @type {boolean | undefined} */ let frozen let freezeIndex = -1 @@ -83,7 +85,7 @@ function base() { } /** - * @param {string|Record} [key] + * @param {Record | string} [key] * @param {unknown} [value] * @returns {unknown} */ @@ -128,7 +130,7 @@ function base() { options[0] = undefined } - /** @type {Transformer|void} */ + /** @type {Transformer | void} */ const transformer = attacher.call(processor, ...options) if (typeof transformer === 'function') { @@ -143,12 +145,12 @@ function base() { } /** - * @param {Pluggable|null|undefined} [value] + * @param {Pluggable | null | undefined} [value] * @param {...unknown} options * @returns {Processor} */ function use(value, ...options) { - /** @type {Record|undefined} */ + /** @type {Record | undefined} */ let settings assertUnfrozen('use', frozen) @@ -168,6 +170,7 @@ function base() { } if (settings) { + // To do: structured clone? namespace.settings = Object.assign(namespace.settings || {}, settings) } @@ -206,12 +209,13 @@ function base() { addList(result.plugins) if (result.settings) { + // To do: structured clone? settings = Object.assign(settings || {}, result.settings) } } /** - * @param {PluggableList|null|undefined} [plugins] + * @param {PluggableList | null | undefined} [plugins] * @returns {void} */ function addList(plugins) { @@ -236,7 +240,7 @@ function base() { */ function addPlugin(plugin, value) { let index = -1 - /** @type {Processor['attachers'][number]|undefined} */ + /** @type {Processor['attachers'][number] | undefined} */ let entry while (++index < attachers.length) { @@ -294,9 +298,9 @@ function base() { /** * @param {Node} node - * @param {VFileCompatible|RunCallback} [doc] + * @param {RunCallback | VFileCompatible} [doc] * @param {RunCallback} [callback] - * @returns {Promise|void} + * @returns {Promise | void} */ function run(node, doc, callback) { assertNode(node) @@ -314,7 +318,7 @@ function base() { executor(null, callback) /** - * @param {null|((node: Node) => void)} resolve + * @param {((node: Node) => void) | null} resolve * @param {(error: Error) => void} reject * @returns {void} */ @@ -323,7 +327,7 @@ function base() { transformers.run(node, vfile(doc), done) /** - * @param {Error|null} error + * @param {Error | null} error * @param {Node} tree * @param {VFile} file * @returns {void} @@ -344,9 +348,9 @@ function base() { /** @type {Processor['runSync']} */ function runSync(node, file) { - /** @type {Node|undefined} */ + /** @type {Node | undefined} */ let result - /** @type {boolean|undefined} */ + /** @type {boolean | undefined} */ let complete processor.run(node, file, done) @@ -357,7 +361,7 @@ function base() { return result /** - * @param {Error|null} [error] + * @param {Error | null} [error] * @param {Node} [tree] * @returns {void} */ @@ -371,7 +375,7 @@ function base() { /** * @param {VFileCompatible} doc * @param {ProcessCallback} [callback] - * @returns {Promise|undefined} + * @returns {Promise | undefined} */ function process(doc, callback) { processor.freeze() @@ -385,14 +389,14 @@ function base() { executor(null, callback) /** - * @param {null|((file: VFile) => void)} resolve - * @param {(error?: Error|null|undefined) => void} reject + * @param {((file: VFile) => void) | null} resolve + * @param {(error?: Error | null | undefined) => void} reject * @returns {void} */ function executor(resolve, reject) { const file = vfile(doc) - processor.run(processor.parse(file), file, (error, tree, file) => { + processor.run(processor.parse(file), file, function (error, tree, file) { if (error || !tree || !file) { done(error) } else { @@ -412,8 +416,8 @@ function base() { }) /** - * @param {Error|null|undefined} [error] - * @param {VFile|undefined} [file] + * @param {Error | null | undefined} [error] + * @param {VFile | undefined} [file] * @returns {void} */ function done(error, file) { @@ -431,7 +435,7 @@ function base() { /** @type {Processor['processSync']} */ function processSync(doc) { - /** @type {boolean|undefined} */ + /** @type {boolean | undefined} */ let complete processor.freeze() @@ -447,7 +451,7 @@ function base() { return file /** - * @param {Error|null|undefined} [error] + * @param {Error | null | undefined} [error] * @returns {void} */ function done(error) { diff --git a/package.json b/package.json index 5b45b7ba..3135cd1e 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "vfile": "^6.0.0" }, "devDependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", "@types/node": "^20.0.0", "@types/ungap__structured-clone": "^0.3.0", "c8": "^8.0.0", @@ -107,7 +109,8 @@ "**/*.ts" ], "rules": { - "@typescript-eslint/ban-types": "off" + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/naming-convention": "off" } } ], diff --git a/readme.md b/readme.md index 7a604451..c3c8e53c 100644 --- a/readme.md +++ b/readme.md @@ -389,7 +389,7 @@ import concatStream from 'concat-stream' import {remark} from 'remark' process.stdin.pipe( - concatStream((buf) => { + concatStream(function (buf) { process.stdout.write(String(remark().processSync(buf))) }) ) diff --git a/test/async-function.js b/test/async-function.js deleted file mode 100644 index bec3d901..00000000 --- a/test/async-function.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @typedef {import('unist').Node} Node - */ - -import assert from 'node:assert/strict' -import test from 'node:test' -import {VFile} from 'vfile' -import {unified} from 'unified' - -test('async function transformer () {}', () => { - const givenFile = new VFile('alpha') - const givenNode = {type: 'bravo'} - const modifiedNode = {type: 'charlie'} - - unified() - .use(() => async function () {}) - .use( - // Note: TS JS doesn’t understand the `Promise` w/o explicit type. - /** @type {import('unified').Plugin<[]>} */ - () => - async function () { - return undefined - } - ) - .use(() => async (tree, file) => { - assert.equal(tree, givenNode, 'passes correct tree to an async function') - assert.equal(file, givenFile, 'passes correct file to an async function') - return modifiedNode - }) - .run(givenNode, givenFile, (error, tree, file) => { - assert.ifError(error) - assert.equal(tree, modifiedNode, 'passes given tree to `done`') - assert.equal(file, givenFile, 'passes given file to `done`') - }) -}) diff --git a/test/core.js b/test/core.js index 1a378cf7..5fc9ffda 100644 --- a/test/core.js +++ b/test/core.js @@ -2,40 +2,32 @@ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' -test('unified()', () => { - /** @type {number} */ - let count - - assert.throws( - () => { - // @ts-expect-error: `use` does not exist on frozen processors. - unified.use(() => {}) - }, - /Cannot call `use` on a frozen processor/, - 'should be frozen' - ) - - const processor = unified() - - assert.equal(typeof processor, 'function', 'should return a function') +test('core', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('unified')).sort(), ['unified']) + }) - processor.use(function () { - count++ - this.data('foo', 'bar') + await t.test('should expose a frozen processor', async function () { + assert.throws(function () { + // @ts-expect-error: check that `use` cannot be used on frozen processors. + unified.use(function () {}) + }, /Cannot call `use` on a frozen processor/) }) - count = 0 - const otherProcessor = processor().freeze() + await t.test( + 'should create a new processor implementing the ancestral processor when called', + async function () { + let count = 0 - assert.equal( - count, - 1, - 'should create a new processor implementing the ancestral processor when called (#1)' - ) + const processor = unified().use(function () { + count++ + this.data('foo', 'bar') + }) + + const otherProcessor = processor().freeze() - assert.equal( - otherProcessor.data('foo'), - 'bar', - 'should create a new processor implementing the ancestral processor when called (#2)' + assert.equal(count, 1) + assert.deepEqual(otherProcessor.data(), {foo: 'bar'}) + } ) }) diff --git a/test/data.js b/test/data.js index 54a6dc3c..e51a786c 100644 --- a/test/data.js +++ b/test/data.js @@ -2,38 +2,39 @@ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' -test('data(key[, value])', () => { - const processor = unified() - - assert.equal( - processor.data('foo', 'bar'), - processor, - 'should return self as setter' - ) - - assert.equal(processor.data('foo'), 'bar', 'should return data as getter') - - assert.equal( - processor.data('toString'), - null, - 'should not return own inherited properties.' - ) - - assert.deepEqual( - processor.data(), - {foo: 'bar'}, - 'should return the memory without arguments' - ) - - assert.deepEqual( - processor.data({baz: 'qux'}), - processor, - 'should set the memory with just a value (#1)' - ) - - assert.deepEqual( - processor.data(), - {baz: 'qux'}, - 'should set the memory with just a value (#2)' - ) +test('`data`', async function (t) { + await t.test('should return self as setter', async function () { + const processor = unified() + + assert.equal(processor.data('foo', 'bar'), processor) + }) + + await t.test('should yield data as getter (not defined)', async function () { + assert.equal(unified().data('foo'), null) + }) + + await t.test('should yield data as getter (defined)', async function () { + assert.equal(unified().data('foo', 'bar').data('foo'), 'bar') + }) + + await t.test('should not yield data prototypal fields', async function () { + assert.equal(unified().data('toString'), null) + }) + + await t.test('should yield dataset as getter w/o key', async function () { + assert.deepEqual(unified().data('foo', 'bar').data(), {foo: 'bar'}) + }) + + await t.test('should set dataset as setter w/o key (#1)', async function () { + const processor = unified().data('foo', 'bar') + + assert.equal(processor.data({baz: 'qux'}), processor) + assert.deepEqual(processor.data(), {baz: 'qux'}) + }) + + await t.test('should set dataset as setter w/o key (#2)', async function () { + assert.deepEqual(unified().data('foo', 'bar').data({baz: 'qux'}).data(), { + baz: 'qux' + }) + }) }) diff --git a/test/freeze.js b/test/freeze.js index b00f4932..fccbbc7c 100644 --- a/test/freeze.js +++ b/test/freeze.js @@ -1,88 +1,216 @@ -/** - * @typedef {import('unified').Plugin} Plugin - */ - import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' import {SimpleCompiler, SimpleParser} from './util/simple.js' -test('freeze()', async (t) => { +test('`freeze`', async function (t) { const frozen = unified() .use(function () { - // Note: TS has a bug so setting `this.Parser` and such doesn’t work, - // but assigning is fine. - Object.assign(this, { - Parser: SimpleParser, - Compiler: SimpleCompiler - }) + this.Parser = SimpleParser + this.Compiler = SimpleCompiler }) .freeze() const unfrozen = frozen() - assert.doesNotThrow(() => { - unfrozen.data() - }, '`data` can be called on unfrozen interfaces') - - assert.throws( - () => { - frozen.data('foo', 'bar') - }, - /Cannot call `data` on a frozen processor/, - '`data` cannot be called on frozen interfaces' - ) - - assert.throws( - () => { - // @ts-expect-error: `use` does not exist on frozen processors. - frozen.use() - }, - /Cannot call `use` on a frozen processor/, - '`use` cannot be called on frozen interfaces' - ) - - assert.doesNotThrow(() => { - frozen.parse() - }, '`parse` can be called on frozen interfaces') - - assert.doesNotThrow(() => { - frozen.stringify({type: 'foo'}) - }, '`stringify` can be called on frozen interfaces') - - assert.doesNotThrow(() => { - frozen.runSync({type: 'foo'}) - }, '`runSync` can be called on frozen interfaces') - - assert.doesNotThrow(() => { - frozen.run({type: 'foo'}, () => {}) - }, '`run` can be called on frozen interfaces') - - assert.doesNotThrow(() => { - frozen.processSync('') - }, '`processSync` can be called on frozen interfaces') - - assert.doesNotThrow(() => { - frozen.process('', () => {}) - }, '`process` can be called on frozen interfaces') - - await t.test('should freeze once, even for nested calls', () => { - let index = 0 - const processor = unified() - .use(() => { - assert.ok(true, 'Expected: ' + String(index++)) - }) - .use({plugins: [freezingPlugin]}) - .use({plugins: [freezingPlugin]}) - .freeze() - - processor().freeze() - - /** - * @this {import('unified').Processor} - * @type {Plugin} - */ - function freezingPlugin() { - this.freeze() - } + await t.test('data', async function (t) { + await t.test( + 'should be able to call `data()` (getter) when not frozen', + async function () { + unfrozen.data() + } + ) + + await t.test( + 'should be able to call `data()` (getter) when frozen', + async function () { + frozen.data() + } + ) + + await t.test( + 'should be able to call `data(key)` (getter) when not frozen', + async function () { + unfrozen.data('x') + } + ) + + await t.test( + 'should be able to call `data(key)` (getter) when frozen', + async function () { + frozen.data('x') + } + ) + + await t.test( + 'should be able to call `data(value)` (setter) when not frozen', + async function () { + unfrozen.data({foo: 'bar'}) + } + ) + + await t.test( + 'should not be able to call `data(value)` (setter) when frozen', + async function () { + assert.throws(function () { + frozen.data({foo: 'bar'}) + }, /Cannot call `data` on a frozen processor/) + } + ) + + await t.test( + 'should be able to call `data(key, value)` (setter) when not frozen', + async function () { + unfrozen.data('foo', 'bar') + } + ) + + await t.test( + 'should not be able to call `data(key, value)` (setter) when frozen', + async function () { + assert.throws(function () { + frozen.data('foo', 'bar') + }, /Cannot call `data` on a frozen processor/) + } + ) + }) + + await t.test('parse', async function (t) { + await t.test( + 'should be able to call `parse` when not frozen', + async function () { + unfrozen.parse() + } + ) + + await t.test( + 'should be able to call `parse` when frozen', + async function () { + frozen.parse() + } + ) + }) + + await t.test('stringify', async function (t) { + await t.test( + 'should be able to call `stringify` when not frozen', + async function () { + unfrozen.stringify({type: 'foo'}) + } + ) + + await t.test( + 'should be able to call `stringify` when frozen', + async function () { + frozen.stringify({type: 'foo'}) + } + ) + }) + + await t.test('run', async function (t) { + await t.test( + 'should be able to call `run` when not frozen', + async function () { + await unfrozen.run({type: 'foo'}) + } + ) + + await t.test('should be able to call `run` when frozen', async function () { + await frozen.run({type: 'foo'}) + }) + }) + + await t.test('runSync', async function (t) { + await t.test( + 'should be able to call `runSync` when not frozen', + async function () { + unfrozen.runSync({type: 'foo'}) + } + ) + + await t.test( + 'should be able to call `runSync` when frozen', + async function () { + frozen.runSync({type: 'foo'}) + } + ) + }) + + await t.test('process', async function (t) { + await t.test( + 'should be able to call `process` when not frozen', + async function () { + await unfrozen.process('') + } + ) + + await t.test( + 'should be able to call `process` when frozen', + async function () { + await frozen.process('') + } + ) + }) + + await t.test('processSync', async function (t) { + await t.test( + 'should be able to call `processSync` when not frozen', + async function () { + unfrozen.processSync('') + } + ) + + await t.test( + 'should be able to call `processSync` when frozen', + async function () { + frozen.processSync('') + } + ) + }) + + await t.test('freeze', async function (t) { + await t.test( + 'should be able to call `freeze` when not frozen', + async function () { + unfrozen.freeze() + } + ) + + await t.test( + 'should be able to call `freeze` when frozen', + async function () { + frozen.freeze() + } + ) + + await t.test('should freeze once, even for nested calls', function () { + let index = 0 + + const processor = unified() + .use(function () { + index++ + }) + .use({plugins: [freezingPlugin]}) + .use({plugins: [freezingPlugin]}) + .freeze() + // To show it doesn’t do anything. + .freeze() + + assert.equal(index, 1) + + processor() + .freeze() + // To show it doesn’t do anything. + .freeze() + + assert.equal(index, 2) + + /** + * @satisfies {import('unified').Plugin<[]>} + * @this {import('unified').Processor} + */ + function freezingPlugin() { + this.freeze() + } + }) }) }) diff --git a/test/index.js b/test/index.js index d60e7cd9..5047cd38 100644 --- a/test/index.js +++ b/test/index.js @@ -1,11 +1,13 @@ /* eslint-disable import/no-unassigned-import */ import './core.js' -import './freeze.js' import './data.js' -import './use.js' +import './freeze.js' import './parse.js' +import './process.js' +import './process-compilers.js' +import './process-sync.js' import './run.js' +import './run-sync.js' import './stringify.js' -import './process.js' -import './async-function.js' +import './use.js' /* eslint-enable import/no-unassigned-import */ diff --git a/test/parse.js b/test/parse.js index eff1326d..3fa472f2 100644 --- a/test/parse.js +++ b/test/parse.js @@ -1,84 +1,90 @@ /** * @typedef {import('unist').Node} Node - * @typedef {import('vfile').VFile} VFile */ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' +import {VFile} from 'vfile' -test('parse(file)', () => { - const processor = unified() - const givenNode = {type: 'delta'} +test('`parse`', async function (t) { + const givenNode = {type: 'alpha'} - assert.throws( - () => { - processor.parse('') - }, - /Cannot `parse` without `Parser`/, - 'should throw without `Parser`' - ) + await t.test('should throw without `Parser`', async function () { + assert.throws(function () { + unified().parse('') + }, /Cannot `parse` without `Parser`/) + }) - processor.Parser = function (doc, file) { - assert.equal(typeof doc, 'string', 'should pass a document') - assert.ok('message' in file, 'should pass a file') - } + await t.test('should support a plain function', async function () { + const processor = unified() - processor.Parser.prototype.parse = function () { - assert.equal(arguments.length, 0, 'should not pass anything to `parse`') - return givenNode - } + processor.Parser = function (doc, file) { + assert.equal(typeof doc, 'string') + assert.ok(file instanceof VFile) + assert.equal(arguments.length, 2) + return givenNode + } - assert.equal( - processor.parse('charlie'), - givenNode, - 'should return the result `Parser#parse` returns' - ) + assert.equal(processor.parse('charlie'), givenNode) + }) - processor.Parser = function (doc, file) { - assert.equal(typeof doc, 'string', 'should pass a document') - assert.ok('message' in file, 'should pass a file') - return givenNode - } + await t.test('should support an arrow function', async function () { + const processor = unified() - assert.equal( - processor.parse('charlie'), - givenNode, - 'should return the result `Parser` returns if it’s not a constructor' - ) + // Note: arrow function intended (which doesn’t have a prototype). + processor.Parser = (doc, file) => { + assert.equal(typeof doc, 'string') + assert.ok(file instanceof VFile) + return givenNode + } - processor.Parser = (doc, file) => { - assert.equal(typeof doc, 'string', 'should pass a document') - assert.ok('message' in file, 'should pass a file') - return givenNode - } + assert.equal(processor.parse('charlie'), givenNode) + }) - assert.equal( - processor.parse('charlie'), - givenNode, - 'should return the result `parser` returns if it’s an arrow function' - ) + await t.test('should support a class', async function () { + const processor = unified() - processor.Parser = class ESParser { - /** - * @param {string} doc - * @param {VFile} file - */ - constructor(doc, file) { - assert.equal(typeof doc, 'string', 'should pass a document') - assert.ok('message' in file, 'should pass a file') - } + processor.Parser = class { + /** + * @param {string} doc + * @param {VFile} file + */ + constructor(doc, file) { + assert.equal(typeof doc, 'string') + assert.ok(file instanceof VFile) + assert.equal(arguments.length, 2) + } - /** @returns {Node} */ - parse() { - assert.equal(arguments.length, 0, 'should not pass anything to `parse`') - return givenNode + /** + * @returns {Node} + */ + parse() { + assert.equal(arguments.length, 0) + return givenNode + } } - } - assert.equal( - processor.parse('charlie'), - givenNode, - 'should return the result `Parser#parse` returns on an ES class' + assert.equal(processor.parse('charlie'), givenNode) + }) + + await t.test( + 'should support a constructor w/ `parse` in prototype', + async function () { + const processor = unified() + + processor.Parser = function (doc, file) { + assert.equal(typeof doc, 'string') + assert.ok(file instanceof VFile) + assert.equal(arguments.length, 2) + } + + processor.Parser.prototype.parse = function () { + assert.equal(arguments.length, 0) + return givenNode + } + + assert.equal(processor.parse('charlie'), givenNode) + } ) }) diff --git a/test/process-compilers.js b/test/process-compilers.js new file mode 100644 index 00000000..46285de5 --- /dev/null +++ b/test/process-compilers.js @@ -0,0 +1,73 @@ +import {Buffer} from 'node:buffer' +import assert from 'node:assert/strict' +import test from 'node:test' +import {unified} from 'unified' +import {SimpleParser} from './util/simple.js' + +test('process (compilers)', async function (t) { + await t.test('should compile `string`', async function () { + const processor = unified() + const result = 'bravo' + + processor.Parser = SimpleParser + processor.Compiler = function () { + return result + } + + const file = await processor.process('') + + assert.equal(file.value, result) + assert.equal(file.result, undefined) + }) + + await t.test('should compile `buffer`', async function () { + const processor = unified() + const result = Buffer.from('bravo') + + processor.Parser = SimpleParser + processor.Compiler = function () { + return result + } + + const file = await processor.process('') + + assert.equal(file.value, result) + assert.equal(file.result, undefined) + }) + + await t.test('should compile `null`', async function () { + const processor = unified() + + processor.Parser = SimpleParser + processor.Compiler = function () { + return null + } + + const file = await processor.process('alpha') + + // To do: is this right? + assert.equal(file.value, 'alpha') + assert.equal(file.result, undefined) + }) + + await t.test('should compile non-text', async function () { + const processor = unified() + const result = { + _owner: null, + type: 'p', + ref: null, + key: 'h-1', + props: {children: ['bravo']} + } + + processor.Parser = SimpleParser + processor.Compiler = function () { + return result + } + + const file = await processor.process('alpha') + + assert.equal(file.value, 'alpha') + assert.equal(file.result, result) + }) +}) diff --git a/test/process-sync.js b/test/process-sync.js new file mode 100644 index 00000000..a315a972 --- /dev/null +++ b/test/process-sync.js @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import {unified} from 'unified' +import {SimpleCompiler, SimpleParser} from './util/simple.js' + +test('`processSync`', async function (t) { + await t.test('should throw w/o `Parser`', async function () { + assert.throws(function () { + unified().processSync('') + }, /Cannot `processSync` without `Parser`/) + }) + + await t.test('should throw w/o `Compiler`', async function () { + assert.throws(function () { + const processor = unified() + processor.Parser = SimpleParser + processor.processSync('') + }, /Cannot `processSync` without `Compiler`/) + }) + + await t.test('should support `processSync`', async function () { + const processor = unified() + + processor.Parser = SimpleParser + processor.Compiler = SimpleCompiler + + assert.equal(processor.processSync('alpha').toString(), 'alpha') + }) + + await t.test( + 'should throw transform errors from `processSync`', + async function () { + assert.throws(function () { + unified() + .use(function () { + this.Parser = SimpleParser + this.Compiler = SimpleCompiler + + return function () { + return new Error('bravo') + } + }) + .processSync('delta') + }, /Error: bravo/) + } + ) +}) diff --git a/test/process.js b/test/process.js index 72dc56ce..b43360c3 100644 --- a/test/process.js +++ b/test/process.js @@ -1,251 +1,96 @@ -/** - * @typedef {import('unist').Literal} Literal - * @typedef {import('unified').Parser} Parser - * @typedef {import('unified').Compiler} Compiler - */ - -import {Buffer} from 'node:buffer' import assert from 'node:assert/strict' import test from 'node:test' -import {VFile} from 'vfile' import {unified} from 'unified' +import {VFile} from 'vfile' import {SimpleCompiler, SimpleParser} from './util/simple.js' -test('process(file, done)', () => { +test('`process`', async function (t) { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} - assert.throws( - () => { + await t.test('should throw w/o `Parser`', async function () { + assert.throws(function () { unified().process('') - }, - /Cannot `process` without `Parser`/, - 'should throw without `Parser`' - ) + }, /Cannot `process` without `Parser`/) + }) - assert.throws( - () => { + await t.test('should throw w/o `Compiler`', async function () { + assert.throws(function () { const processor = unified() processor.Parser = SimpleParser processor.process('') - }, - /Cannot `process` without `Compiler`/, - 'should throw without `Compiler`' - ) - - unified() - .use(function () { - Object.assign(this, { - /** @type {Parser} */ - Parser(doc, file) { - assert.equal(typeof doc, 'string', 'should pass `doc` to `Parser`') - assert.equal(file, givenFile, 'should pass `file` to `Parser`') - return givenNode - } - }) - }) - .use( - () => - function (tree, file) { - assert.equal(tree, givenNode, 'should pass `tree` to transformers') - assert.equal(file, givenFile, 'should pass `file` to transformers') - } - ) - .use(function () { - Object.assign(this, { - /** @type {Compiler} */ - Compiler(tree, file) { - assert.equal(tree, givenNode, 'should pass `tree` to `Compiler`') - assert.equal(file, givenFile, 'should pass `file` to `Compiler`') - return 'charlie' - } - }) - }) - .process(givenFile, (error, file) => { - assert.ifError(error) - - assert.equal( - String(file), - 'charlie', - 'should store the result of `compile()` on `file`' - ) + }, /Cannot `process` without `Compiler`/) + }) + + await t.test('should pass/yield expected values', async function () { + const processor = unified() + + processor.Parser = function (doc, file) { + assert.equal(typeof doc, 'string') + assert.equal(file, givenFile) + assert.equal(arguments.length, 2) + return givenNode + } + + processor.Compiler = function (tree, file) { + assert.equal(tree, givenNode) + assert.equal(file, givenFile) + assert.equal(arguments.length, 2) + return 'charlie' + } + + processor.use(function () { + return function (tree, file) { + assert.equal(tree, givenNode) + assert.equal(file, givenFile) + assert.equal(arguments.length, 2) + } }) - assert.throws(() => { - unified() - .use(function () { - Object.assign(this, {Parser: SimpleParser, Compiler: SimpleCompiler}) - }) - .process(givenFile, () => { - throw new Error('Alfred') - }) - }, /^Error: Alfred$/) -}) - -test('process(file)', () => { - const givenFile = new VFile('alpha') - const givenNode = {type: 'bravo'} - - unified() - .use(function () { - Object.assign(this, { - /** @type {Parser} */ - Parser(doc, file) { - assert.equal(typeof doc, 'string', 'should pass `doc` to `Parser`') - assert.equal(file, givenFile, 'should pass `file` to `Parser`') - return givenNode - } - }) - }) - .use( - () => - function (tree, file) { - assert.equal(tree, givenNode, 'should pass `tree` to transformers') - assert.equal(file, givenFile, 'should pass `file` to transformers') - } - ) - .use(function () { - Object.assign(this, { - /** @type {Compiler} */ - Compiler(tree, file) { - assert.equal(tree, givenNode, 'should pass `tree` to `Compiler`') - assert.equal(file, givenFile, 'should pass `file` to `Compiler`') - return 'charlie' - } + await new Promise(function (resolve) { + processor.process(givenFile, function (error, file) { + assert.ifError(error) + assert.equal(String(file), 'charlie') + resolve(undefined) }) }) - .process(givenFile) - .then( - (file) => { - assert.equal(file.toString(), 'charlie', 'should resolve the file') - }, - () => { - assert.fail('should resolve, not reject, the file') - } - ) -}) + }) -test('processSync(file)', () => { - assert.throws( - () => { - unified().processSync('') - }, - /Cannot `processSync` without `Parser`/, - 'should throw without `Parser`' - ) - - assert.throws( - () => { - const processor = unified() - processor.Parser = SimpleParser - processor.processSync('') - }, - /Cannot `processSync` without `Compiler`/, - 'should throw without `Compiler`' - ) - - assert.throws( - () => { - unified() - .use(function () { - Object.assign(this, {Parser: SimpleParser, Compiler: SimpleCompiler}) - return function () { - return new Error('bravo') - } - }) - .processSync('delta') - }, - /Error: bravo/, - 'should throw error from `processSync`' - ) + await t.test('should rethrow errors in `done` throws', async function () { + const processor = unified() - assert.equal( - unified() - .use(function () { - Object.assign(this, {Parser: SimpleParser, Compiler: SimpleCompiler}) - return function (node) { - const text = /** @type {Literal} */ (node) - text.value = 'alpha' - } - }) - .processSync('delta') - .toString(), - 'alpha', - 'should pass the result file' - ) -}) + processor.Parser = SimpleParser + processor.Compiler = SimpleCompiler -test('compilers', () => { - assert.equal( - unified() - .use(function () { - Object.assign(this, { - Parser: SimpleParser, - Compiler() { - return 'bravo' - } - }) - }) - .processSync('alpha').value, - 'bravo', - 'should compile strings' - ) - - assert.deepEqual( - unified() - .use(function () { - Object.assign(this, { - Parser: SimpleParser, - Compiler() { - return Buffer.from('bravo') - } - }) + assert.throws(function () { + processor.process(givenFile, function () { + throw new Error('Alfred') }) - .processSync('alpha').value, - Buffer.from('bravo'), - 'should compile buffers' - ) + }, /^Error: Alfred$/) + }) - assert.deepEqual( - unified() - .use(function () { - Object.assign(this, { - Parser: SimpleParser, - Compiler() { - return null - } - }) - }) - .processSync('alpha').value, - 'alpha', - 'should compile null' - ) + await t.test( + 'should support `process` w/o `done` (promise)', + async function () { + const processor = unified() - assert.deepEqual( - unified() - .use(function () { - Object.assign(this, { - Parser: SimpleParser, - Compiler() { - // Somewhat like a React node. - return { - _owner: null, - type: 'p', - ref: null, - key: 'h-1', - props: {children: ['bravo']} - } + processor.Parser = SimpleParser + processor.Compiler = SimpleCompiler + + await new Promise(function (resolve, reject) { + processor.process(givenFile).then( + function (file) { + assert.equal(String(file), 'charlie') + resolve(undefined) + }, + /** + * @param {unknown} error + */ + function (error) { + reject(error) } - }) + ) }) - .processSync('alpha').result, - { - _owner: null, - type: 'p', - ref: null, - key: 'h-1', - props: {children: ['bravo']} - }, - 'should compile non-text' + } ) }) diff --git a/test/run-sync.js b/test/run-sync.js new file mode 100644 index 00000000..6af4bb32 --- /dev/null +++ b/test/run-sync.js @@ -0,0 +1,188 @@ +import process from 'node:process' +import assert from 'node:assert/strict' +import test from 'node:test' +import {unified} from 'unified' +import {VFile} from 'vfile' + +test('`runSync`', async function (t) { + const givenFile = new VFile('alpha') + const givenNode = {type: 'bravo'} + const otherNode = {type: 'charlie'} + const givenError = new Error('delta') + + await t.test('should throw w/o `tree`', async function () { + assert.throws(function () { + unified() + // @ts-expect-error: check how missing `node` is handled. + .runSync() + }, /Expected node, got `undefined`/) + }) + + await t.test('should pass/yield expected values', async function () { + assert.equal(unified().runSync(givenNode, givenFile), givenNode) + }) + + await t.test( + 'should throw an error returned from a sync transformer', + async function () { + assert.throws(function () { + unified() + .use(function () { + return function () { + return givenError + } + }) + .runSync(givenNode) + }, givenError) + } + ) + + await t.test( + 'should yield a tree when returned from a sync transformer', + async function () { + assert.equal( + unified() + .use(function () { + return function () { + return otherNode + } + }) + .runSync(givenNode), + otherNode + ) + } + ) + + await t.test( + 'should throw an error when passed to a transformer’s `next` (sync)', + async function () { + assert.throws(function () { + unified() + .use(function () { + return function (_1, _2, next) { + next(givenError) + } + }) + .runSync(givenNode) + }, givenError) + } + ) + + await t.test( + 'should throw and leave an error uncaught when passed to a transformer’s `next` (async)', + async function () { + await new Promise(function (resolve) { + // @ts-expect-error: prevent the test runner from warning. + const events = /** @type {Record} */ (process._events) + const current = events.uncaughtException + events.uncaughtException = undefined + + process.once('uncaughtException', function (error) { + assert.equal(error, givenError) + events.uncaughtException = current + resolve(undefined) + }) + + assert.throws(function () { + unified() + .use(function () { + return function (_1, _2, next) { + setImmediate(tick) + + function tick() { + next(givenError) + } + } + }) + .runSync(givenNode) + }, /`runSync` finished async. Use `run` instead/) + }) + } + ) + + await t.test( + 'should yield a tree when passed to a transformer’s `next` (sync)', + async function () { + assert.equal( + unified() + .use(function () { + return function (_1, _2, next) { + next(undefined, otherNode) + } + }) + .runSync(givenNode), + otherNode + ) + } + ) + + await t.test( + 'should throw and ignore a tree when passed to a transformer’s `next` (async)', + async function () { + assert.throws(function () { + unified() + .use(function () { + return function (_1, _2, next) { + setImmediate(tick) + + function tick() { + next(undefined, otherNode) + } + } + }) + .runSync(givenNode) + }, /`runSync` finished async. Use `run` instead/) + } + ) + + await t.test( + 'should throw and leave an error unhandled when rejected from a transformer', + async function () { + await new Promise(function (resolve) { + // @ts-expect-error: prevent the test runner from warning. + const events = /** @type {Record} */ (process._events) + const current = events.unhandledRejection + events.unhandledRejection = undefined + + process.once('unhandledRejection', function (error) { + assert.equal(error, givenError) + events.unhandledRejection = current + resolve(undefined) + }) + + assert.throws(function () { + unified() + .use( + // Note: TS doesn’t understand `Promise`. + /** + * @type {import('unified').Plugin<[]>} + */ + function () { + return async function () { + throw givenError + } + } + ) + .runSync(givenNode) + }, /`runSync` finished async. Use `run` instead/) + }) + } + ) + + await t.test( + 'should throw and ignore a tree when resolved from a transformer', + async function () { + assert.throws(function () { + unified() + .use(function () { + return function () { + return new Promise(function (resolve) { + resolve(otherNode) + }) + } + }) + .runSync(givenNode) + }, /`runSync` finished async. Use `run` instead/) + } + ) +}) diff --git a/test/run.js b/test/run.js index df77a08c..c4caad54 100644 --- a/test/run.js +++ b/test/run.js @@ -1,699 +1,587 @@ -import process from 'node:process' import assert from 'node:assert/strict' import test from 'node:test' -import {VFile} from 'vfile' import {unified} from 'unified' +import {VFile} from 'vfile' -test('run(node[, file], done)', async () => { +test('`run`', async function (t) { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} - const otherNode = {type: 'delta'} - - await new Promise((resolve) => { - unified().run(givenNode, givenFile, (error, tree, file) => { - assert.ifError(error) - assert.equal(tree, givenNode, 'passes given tree to `done`') - assert.equal(file, givenFile, 'passes given file to `done`') - resolve(undefined) - }) - }) - - await new Promise((resolve) => { - unified().run(givenNode, undefined, (error, _, file) => { - assert.ifError(error) - assert.equal(String(file), '', 'passes file to `done` if not given') - resolve(undefined) + const otherNode = {type: 'charlie'} + const givenError = new Error('delta') + + await t.test('should pass/yield expected values', async function () { + await new Promise(function (resolve) { + unified().run(givenNode, givenFile, function (error, tree, file) { + assert.equal(error, null) + assert.equal(tree, givenNode) + assert.equal(file, givenFile) + assert.equal(arguments.length, 3) + resolve(undefined) + }) }) }) - await new Promise((resolve) => { - unified().run(givenNode, (error, _, file) => { - assert.ifError(error) - assert.equal(String(file), '', 'passes file to `done` if omitted') - resolve(undefined) + await t.test('should pass a file if implicitly not given', async function () { + await new Promise(function (resolve) { + unified().run(givenNode, function (error, _, file) { + assert.equal(error, null) + assert.ok(file instanceof VFile) + resolve(undefined) + }) }) }) - await new Promise((resolve) => { - unified() - .use( - () => - function () { - return new Error('charlie') - } - ) - .run(givenNode, (error) => { - assert.equal( - String(error), - 'Error: charlie', - 'should pass an error to `done` from a sync transformer' - ) + await t.test('should pass a file if explicitly not given', async function () { + await new Promise(function (resolve) { + unified().run(givenNode, undefined, function (error, _, file) { + assert.equal(error, null) + assert.ok(file instanceof VFile) resolve(undefined) }) + }) }) - await new Promise((resolve) => { - unified() - .use(() => () => otherNode) - .run(givenNode, (error, tree) => { - assert.ifError(error) - - assert.equal( - tree, - otherNode, - 'should pass a new tree to `done`, when returned from a sync transformer' - ) - resolve(undefined) + await t.test( + 'should yield an error returned from a sync transformer', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function () { + return givenError + } + }) + .run(givenNode, function (error) { + assert.equal(error, givenError) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - next(new Error('delta')) - } - ) - .run(givenNode, (error) => { - assert.equal( - String(error), - 'Error: delta', - 'should pass an error to `done`, if given to a sync transformer’s `next`' - ) - resolve(undefined) + await t.test( + 'should yield a tree when returned from a sync transformer', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function () { + return otherNode + } + }) + .run(givenNode, function (error, tree) { + assert.equal(error, null) + assert.equal(tree, otherNode) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - next() - next(new Error('delta')) - } - ) - .run(givenNode, (error) => { - assert.ifError(error) - resolve(undefined) + await t.test( + 'should yield an error when passed to a transformer’s `next` (sync)', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function (_1, _2, next) { + next(givenError) + } + }) + .run(givenNode, function (error) { + assert.equal(error, givenError) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - next(null, otherNode) - } - ) - .run(givenNode, (error, tree) => { - assert.ifError(error) - - assert.equal( - tree, - otherNode, - 'should pass a new tree to `done`, if given to a sync transformer’s `next`' - ) - resolve(undefined) + await t.test( + 'should yield an error when passed to a transformer’s `next` (async)', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function (_1, _2, next) { + setImmediate(tick) + function tick() { + next(givenError) + } + } + }) + .run(givenNode, function (error) { + assert.equal(error, givenError) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function () { - return new Promise((_, reject) => { - reject(new Error('delta')) - }) - } - ) - .run(givenNode, (error) => { - assert.equal( - String(error), - 'Error: delta', - 'should pass an error to `done` rejected from a sync transformer’s returned promise' - ) - resolve(undefined) + await t.test( + 'should yield a tree when passed to a transformer’s `next` (sync)', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function (_1, _2, next) { + next(undefined, otherNode) + } + }) + .run(givenNode, function (error, tree) { + assert.equal(error, null) + assert.equal(tree, otherNode) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('unified').Plugin<[]>} */ - () => - function () { - return new Promise((resolve) => { - resolve(otherNode) - }) - } - ) - .run(givenNode, (error, tree) => { - assert.ifError(error) - - assert.equal( - tree, - otherNode, - 'should pass a new tree to `done`, when resolved sync transformer’s returned promise' - ) - resolve(undefined) + await t.test( + 'should yield a tree when passed to a transformer’s `next` (async)', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function (_1, _2, next) { + setImmediate(tick) + function tick() { + next(undefined, otherNode) + } + } + }) + .run(givenNode, function (error, tree) { + assert.equal(error, null) + assert.equal(tree, otherNode) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(tick) - function tick() { - next(null, otherNode) + await t.test( + 'should yield an error when rejected from a transformer', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function () { + return new Promise(function (_, reject) { + reject(givenError) + }) } - } - ) - .run(givenNode, (error, tree) => { - assert.ifError(error) - - assert.equal( - tree, - otherNode, - 'should pass a new tree to `done` when given to `next` from an asynchroneous transformer' - ) - resolve(undefined) + }) + .run(givenNode, function (error) { + assert.equal(error, givenError) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(tick) - function tick() { - next(new Error('echo')) + await t.test( + 'should yield a tree when resolved from a transformer', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function () { + return new Promise(function (resolve) { + resolve(otherNode) + }) } - } - ) - .run(givenNode, (error) => { - assert.equal( - String(error), - 'Error: echo', - 'should pass an error to `done` given to `next` from an asynchroneous transformer' - ) - resolve(undefined) + }) + .run(givenNode, function (error, tree) { + assert.equal(error, null) + assert.equal(tree, otherNode) + resolve(undefined) + }) }) - }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(tick) - function tick() { + await t.test( + 'should swallow further errors when passed to a transformer’s `next` (sync)', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function (_1, _2, next) { next() - next(new Error('echo')) + // To do: should this actually throw? + next(new Error('delta')) } - } - ) - .run(givenNode, (error) => { - assert.ifError(error) - resolve(undefined) + }) + .run(givenNode, function (error) { + assert.equal(error, null) + resolve(undefined) + }) }) - }) -}) + } + ) -test('run(node[, file])', async () => { - const givenFile = new VFile('alpha') - const givenNode = {type: 'bravo'} - const otherNode = {type: 'delta'} - - await new Promise((resolve) => { - unified() - .run(givenNode, givenFile) - .then( - (tree) => { - assert.equal(tree, givenNode, 'should resolve the given tree') - resolve(undefined) - }, - () => { - assert.fail('should resolve, not reject, when `file` is given') - resolve(undefined) - } - ) - }) + await t.test( + 'should swallow further errors when passed to a transformer’s `next` (async)', + async function () { + await new Promise(function (resolve) { + unified() + .use(function () { + return function (_1, _2, next) { + setImmediate(tick1) - await new Promise((resolve) => { - unified() - .run(givenNode, undefined) - .then( - (tree) => { - assert.equal(tree, givenNode, 'should work if `file` is not given') - resolve(undefined) - }, - () => { - assert.fail('should resolve, not reject, when `file` is not given') - resolve(undefined) - } - ) - }) + function tick1() { + next() + setImmediate(tick2) + } - await new Promise((resolve) => { - unified() - .run(givenNode) - .then( - (tree) => { - assert.equal(tree, givenNode, 'should work if `file` is omitted') - resolve(undefined) - }, - () => { - assert.fail('should resolve, not reject, when `file` is omitted') - resolve(undefined) - } - ) - }) + function tick2() { + next(new Error('echo')) + } + } + }) + .run(givenNode, function (error) { + assert.equal(error, null) + resolve(undefined) + }) + }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function () { - return new Error('charlie') - } - ) - .run(givenNode) - .then( - () => { - assert.fail( - 'should reject, not resolve, when an error is passed to `done` from a sync transformer' - ) - resolve(undefined) - }, - (/** @type {Error} */ error) => { - assert.equal( - String(error), - 'Error: charlie', - 'should reject when an error is returned from a sync transformer' - ) - resolve(undefined) - } - ) - }) + // To do: test to swallow further trees? - await new Promise((resolve) => { - unified() - .use( - () => + await t.test('should support `async function`s', async function () { + await new Promise(function (resolve) { + unified() + // Async transformer w/o return statement. + .use(function () { + return async function () {} + }) + // Async transformer w/ explicit `undefined`. + .use( + // Note: TS doesn’t understand w/o explicit `this` type. + /** + * @satisfies {import('unified').Plugin<[]>} + * @this {import('unified').Processor} + */ function () { - return otherNode - } - ) - .run(givenNode) - .then( - (tree) => { - assert.equal( - tree, - otherNode, - 'should resolve a new tree when returned from a sync transformer' - ) - resolve(undefined) - }, - () => { - assert.fail( - 'should resolve, not reject, when a new tree is given from a sync transformer' - ) - resolve(undefined) - } - ) - }) - - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - next(new Error('delta')) + return async function () { + return undefined + } } - ) - .run(givenNode) - .then( - () => { - assert.fail( - 'should reject, not resolve, if an error is given to a sync transformer’s `next`' - ) - resolve(undefined) - }, - (/** @type {Error} */ error) => { - assert.equal( - String(error), - 'Error: delta', - 'should reject, if an error is given to a sync transformer’s `next`' - ) - resolve(undefined) - } - ) - }) - - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - next() - next(new Error('delta')) + ) + .use(function () { + return async function (tree, file) { + assert.equal(tree, givenNode) + assert.equal(file, givenFile) + assert.equal(arguments.length, 2) + return otherNode } - ) - .run(givenNode) - .then( - function () { - resolve(undefined) - }, - () => { - assert.fail( - 'should ignore multiple calls of `next` when called in a synchroneous transformer' - ) + }) + .run(givenNode, givenFile, function (error, tree, file) { + assert.equal(error, null) + assert.equal(tree, otherNode) + assert.equal(file, givenFile) resolve(undefined) - } - ) + }) + }) }) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - next(null, otherNode) - } - ) - .run(givenNode) - .then( - (tree) => { - assert.equal( - tree, - otherNode, - 'should resolve if a new tree is given to a sync transformer’s `next`' - ) - resolve(undefined) - }, - () => { - assert.fail( - 'should resolve, not reject, if a new tree is given to a sync transformer’s `next`' - ) - resolve(undefined) - } - ) - }) + await t.test( + 'should pass/yield expected values (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .run(givenNode, givenFile) + .then(function (tree) { + assert.equal(tree, givenNode) + resolve(undefined) + }, reject) + }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function () { - return new Promise((_, reject) => { - reject(new Error('delta')) - }) - } - ) - .run(givenNode) - .then( - () => { - assert.fail( - 'should reject, not resolve, if an error is rejected from a sync transformer’s returned promise' - ) - resolve(undefined) - }, - function () { - resolve(undefined) - } - ) - }) + await t.test( + 'should pass a file if implicitly not given (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .run(givenNode, undefined) + .then(function (tree) { + assert.equal(tree, givenNode) + resolve(undefined) + }, reject) + }) + } + ) - await new Promise((resolve) => { - unified() - .use( - // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('unified').Plugin<[]>} */ - () => - function () { - return new Promise((resolve) => { - resolve(otherNode) - }) - } - ) - .run(givenNode) - .then( - function () { - resolve(undefined) - }, - () => { - assert.fail( - 'should resolve, not reject, a new tree if it’s resolved from a sync transformer’s returned promise' - ) - resolve(undefined) - } - ) - }) + await t.test( + 'should pass a file if explicitly not given (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .run(givenNode) + .then(function (tree) { + assert.equal(tree, givenNode) + resolve(undefined) + }, reject) + }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next(null, otherNode) - }) - } - ) - .run(givenNode) - .then( - function () { - resolve(undefined) - }, - () => { - assert.fail( - 'should resolve, not reject, if a new tree is given to `next` from an asynchroneous transformer' + await t.test( + 'should yield an error returned from a sync transformer (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function () { + return givenError + } + }) + .run(givenNode) + .then( + function () { + reject(new Error('should reject')) + }, + function (error) { + assert.equal(error, givenError) + resolve(undefined) + } ) - resolve(undefined) - } - ) - }) + }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next(new Error('echo')) - }) - } - ) - .run(givenNode) - .then( - () => { - assert.fail( - 'should reject, not resolve, if an error is given to `next` from an asynchroneous transformer' - ) - resolve(undefined) - }, - function () { - resolve(undefined) - } - ) - }) + await t.test( + 'should yield a tree when returned from a sync transformer (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function () { + return otherNode + } + }) + .run(givenNode) + .then(function (tree) { + assert.equal(tree, otherNode) + resolve(undefined) + }, reject) + }) + } + ) - await new Promise((resolve) => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next() - next(new Error('echo')) - }) - } - ) - .run(givenNode) - .then( - function () { - resolve(undefined) - }, - () => { - assert.fail( - 'should ignore multiple calls of `next` when called from an asynchroneous transformer' + await t.test( + 'should yield an error when passed to a transformer’s `next` (sync) (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function (_1, _2, next) { + next(givenError) + } + }) + .run(givenNode) + .then( + function () { + reject(new Error('should reject')) + }, + function (error) { + assert.equal(error, givenError) + resolve(undefined) + } ) - resolve(undefined) - } - ) - }) -}) - -test('runSync(node[, file])', async () => { - const givenFile = new VFile('alpha') - const givenNode = {type: 'bravo'} - const otherNode = {type: 'delta'} - - assert.throws( - () => { - // @ts-expect-error: `node` is required. - unified().runSync() - }, - /Expected node, got `undefined`/, - 'should throw without node' + }) + } ) - unified() - .use( - () => - function (tree, file) { - assert.equal(tree, givenNode, 'passes given tree to transformers') - assert.equal(file, givenFile, 'passes given file to transformers') - } - ) - .runSync(givenNode, givenFile) - - unified() - .use( - () => - function (_, file) { - assert.equal( - file.toString(), - '', - 'passes files to transformers if not given' + await t.test( + 'should yield an error when passed to a transformer’s `next` (async) (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function (_1, _2, next) { + setImmediate(function () { + next(givenError) + }) + } + }) + .run(givenNode) + .then( + function () { + reject(new Error('should reject')) + }, + function (error) { + assert.equal(error, givenError) + resolve(undefined) + } ) - } - ) - .runSync(givenNode) + }) + } + ) - assert.throws( - () => { - unified() - .use( - () => - function () { - return new Error('charlie') + await t.test( + 'should yield a tree when passed to a transformer’s `next` (sync) (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function (_1, _2, next) { + next(undefined, otherNode) } - ) - .runSync(givenNode) - }, - /charlie/, - 'should throw an error returned from a sync transformer' + }) + .run(givenNode) + .then(function (tree) { + assert.equal(tree, otherNode) + resolve(undefined) + }, reject) + }) + } ) - assert.equal( - unified() - .use( - () => - function () { - return otherNode - } - ) - .runSync(givenNode), - otherNode, - 'should return a new tree when returned from a sync transformer' + await t.test( + 'should yield a tree when passed to a transformer’s `next` (async) (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function (_1, _2, next) { + setImmediate(function () { + next(undefined, otherNode) + }) + } + }) + .run(givenNode) + .then(function (tree) { + assert.equal(tree, otherNode) + resolve(undefined) + }, reject) + }) + } ) - assert.throws( - () => { - unified() - .use( - () => - function (_, _1, next) { - next(new Error('delta')) + await t.test( + 'should yield an error when rejected from a transformer (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function () { + return new Promise(function (_, reject) { + reject(givenError) + }) } - ) - .runSync(givenNode) - }, - /delta/, - 'should throw an error if given to a sync transformer’s `next`' + }) + .run(givenNode) + .then( + function () { + reject(new Error('should reject')) + }, + function (error) { + assert.equal(error, givenError) + resolve(undefined) + } + ) + }) + } ) - assert.equal( - unified() - .use( - () => - function (_, _1, next) { - next(null, otherNode) - } - ) - .runSync(givenNode), - otherNode, - 'should return a new tree if given to a sync transformer’s `next`' + await t.test( + 'should yield a tree when resolved from a transformer (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function () { + return new Promise(function (resolve) { + resolve(otherNode) + }) + } + }) + .run(givenNode) + .then(function (tree) { + assert.equal(tree, otherNode) + resolve(undefined) + }, reject) + }) + } ) - await new Promise((resolve) => { - /** @type {unknown} */ - // @ts-expect-error: prevent the test runner from warning. - const current = process._events.unhandledRejection - // @ts-expect-error: prevent the test runner from warning. - process._events.unhandledRejection = undefined - - process.once('unhandledRejection', function () { - resolve(undefined) - // @ts-expect-error: prevent the test runner from warning. - process._events.unhandledRejection = current - }) + await t.test( + 'should swallow further errors when passed to a transformer’s `next` (sync) (promise)', + async function () { + await new Promise(function (resolve, reject) { + unified() + .use(function () { + return function (_1, _2, next) { + next() + next(givenError) + } + }) + .run(givenNode) + .then(function (tree) { + assert.equal(tree, givenNode) + resolve(undefined) + }, reject) + }) + } + ) - assert.throws( - () => { + await t.test( + 'should swallow further errors when passed to a transformer’s `next` (async) (promise)', + async function () { + await new Promise(function (resolve, reject) { unified() - .use( - () => - function () { - return new Promise((_, reject) => { - reject(new Error('delta')) - }) - } - ) - .runSync(givenNode) - }, - /`runSync` finished async. Use `run` instead/, - 'should not support a promise returning transformer rejecting in `runSync`' - ) - }) + .use(function () { + return function (_1, _2, next) { + setImmediate(function () { + next() + next(givenError) + }) + } + }) + .run(givenNode) + .then(function (tree) { + assert.equal(tree, givenNode) + resolve(undefined) + }, reject) + }) + } + ) - assert.throws( - () => { + await t.test('should support `async function`s (promise)', async function () { + await new Promise(function (resolve, reject) { unified() + // Async transformer w/o return statement. + .use(function () { + return async function () {} + }) + // Async transformer w/ explicit `undefined`. .use( - // Note: TS JS doesn’t understand the promise w/o explicit type. - /** @type {import('unified').Plugin<[]>} */ - () => - function () { - return new Promise((resolve) => { - resolve(otherNode) - }) + // Note: TS doesn’t understand w/o explicit `this` type. + /** + * @satisfies {import('unified').Plugin<[]>} + * @this {import('unified').Processor} + */ + function () { + return async function () { + return undefined } + } ) - .runSync(givenNode) - }, - /`runSync` finished async. Use `run` instead/, - 'should not support a promise returning transformer resolving in `runSync`' - ) - - await new Promise((resolve) => { - assert.throws( - () => { - unified() - .use( - () => - function (_, _1, next) { - setImmediate(() => { - next(null, otherNode) - setImmediate(() => { - resolve(undefined) - }) - }) - } - ) - .runSync(givenNode) - }, - /`runSync` finished async. Use `run` instead/, - 'should throw an error if an asynchroneous transformer is used but no `done` is given' - ) + .use(function () { + return async function (tree, file) { + assert.equal(tree, givenNode) + assert.equal(file, givenFile) + assert.equal(arguments.length, 2) + return otherNode + } + }) + .run(givenNode, givenFile) + .then(function (tree) { + assert.equal(tree, otherNode) + resolve(undefined) + }, reject) + }) }) }) diff --git a/test/stringify.js b/test/stringify.js index b1cb975f..652c052a 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -4,83 +4,84 @@ import assert from 'node:assert/strict' import test from 'node:test' -import {VFile} from 'vfile' import {unified} from 'unified' +import {VFile} from 'vfile' -test('stringify(node[, file])', () => { - const processor = unified() - const givenFile = new VFile('charlie') - const givenNode = {type: 'delta'} - - assert.throws( - () => { - processor.stringify({type: 'x'}) - }, - /Cannot `stringify` without `Compiler`/, - 'should throw without `Compiler`' - ) +test('`stringify`', async function (t) { + const givenFile = new VFile('alpha') + const givenNode = {type: 'bravo'} - processor.Compiler = function (node, file) { - assert.equal(node, givenNode, 'should pass a node') - assert.ok('message' in file, 'should pass a file') - } - - // `prototype`s are objects. - // type-coverage:ignore-next-line - processor.Compiler.prototype.compile = function () { - assert.equal(arguments.length, 0, 'should not pass anything to `compile`') - return 'echo' - } - - assert.equal( - processor.stringify(givenNode, givenFile), - 'echo', - 'should return the result `Compiler#compile` returns' - ) + await t.test('should throw without `Compiler`', async function () { + assert.throws(function () { + unified().stringify(givenNode) + }, /Cannot `stringify` without `Compiler`/) + }) - processor.Compiler = function (node, file) { - assert.equal(node, givenNode, 'should pass a node') - assert.ok('message' in file, 'should pass a file') - return 'echo' - } + await t.test('should support a plain function', async function () { + const processor = unified() - assert.equal( - processor.stringify(givenNode, givenFile), - 'echo', - 'should return the result `compiler` returns if it’s not a constructor' - ) + processor.Compiler = function (node, file) { + assert.equal(node, givenNode) + assert.ok(file instanceof VFile) + assert.equal(arguments.length, 2) + return 'echo' + } - processor.Compiler = (node, file) => { - assert.equal(node, givenNode, 'should pass a node') - assert.ok('message' in file, 'should pass a file') - return 'echo' - } + assert.equal(processor.stringify(givenNode, givenFile), 'echo') + }) - assert.equal( - processor.stringify(givenNode, givenFile), - 'echo', - 'should return the result `compiler` returns if it’s an arrow function' - ) + await t.test('should support an arrow function', async function () { + const processor = unified() - processor.Compiler = class ESCompiler { - /** - * @param {Node} node - * @param {VFile} file - */ - constructor(node, file) { + // Note: arrow function intended (which doesn’t have a prototype). + processor.Compiler = (node, file) => { assert.equal(node, givenNode, 'should pass a node') - assert.ok('message' in file, 'should pass a file') + assert.ok(file instanceof VFile, 'should pass a file') + return 'echo' } - compile() { - assert.equal(arguments.length, 0, 'should not pass anything to `compile`') - return 'echo' + assert.equal(processor.stringify(givenNode, givenFile), 'echo') + }) + + await t.test('should support a class', async function () { + const processor = unified() + + processor.Compiler = class { + /** + * @param {Node} node + * @param {VFile} file + */ + constructor(node, file) { + assert.equal(node, givenNode) + assert.ok(file instanceof VFile) + } + + compile() { + assert.equal(arguments.length, 0) + return 'echo' + } } - } - assert.equal( - processor.stringify(givenNode, givenFile), - 'echo', - 'should return the result `Compiler#compile` returns on an ES class' + assert.equal(processor.stringify(givenNode, givenFile), 'echo') + }) + + await t.test( + 'should support a constructor w/ `compile` in prototype', + async function () { + const processor = unified() + + processor.Compiler = function (node, file) { + assert.equal(node, givenNode, 'should pass a node') + assert.ok(file instanceof VFile, 'should pass a file') + assert.equal(arguments.length, 2) + } + + processor.Compiler.prototype.compile = function () { + assert.equal(arguments.length, 0) + return 'echo' + } + + assert.equal(processor.stringify(givenNode, givenFile), 'echo') + } ) }) diff --git a/test/use.js b/test/use.js index 5e672af8..6ea35de9 100644 --- a/test/use.js +++ b/test/use.js @@ -2,317 +2,253 @@ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' -test('use(plugin[, options])', async (t) => { - await t.test('should ignore missing values', () => { +test('`use`', async function (t) { + const givenOptions = {alpha: 'bravo', charlie: true, delta: 1} + const otherOptions = {echo: [1, 2, 3], foxtrot: {golf: 1}} + const givenOptionsList = [1, 2, 3] + const otherOptionsList = [1, 4, 5] + const mergedOptions = {...givenOptions, ...otherOptions} + + await t.test('should ignore no value', function () { const processor = unified() - // @ts-expect-error: runtime feature. - assert.equal(processor.use(), processor, 'missing') - // @ts-expect-error: runtime feature. - assert.equal(processor.use(null), processor, '`null`') - // @ts-expect-error: runtime feature. - assert.equal(processor.use(undefined), processor, '`undefined`') + // To do: investigate if we can enable it. + // @ts-expect-error: check how the runtime handles a missing value. + assert.equal(processor.use(), processor) }) - await t.test('should throw when given invalid values', () => { - assert.throws( - () => { - // @ts-expect-error: runtime. - unified().use(false) - }, - /^TypeError: Expected usable value, not `false`$/, - '`false`' - ) + await t.test('should ignore `undefined`', function () { + const processor = unified() + // To do: investigate if we can enable it. + // @ts-expect-error: check how the runtime handles `undefined`. + assert.equal(processor.use(undefined), processor) + }) + + await t.test('should ignore `null`', function () { + const processor = unified() + // To do: investigate if we can enable it. + // @ts-expect-error: check how the runtime handles `null`. + assert.equal(processor.use(null), processor) + }) + + await t.test('should throw when given invalid values (`false`)', function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles `false`. + unified().use(false) + }, /Expected usable value, not `false`/) + }) - assert.throws( - () => { - // @ts-expect-error: runtime. + await t.test( + 'should throw when given invalid values (`true`)', + async function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles `true`. unified().use(true) - }, - /^TypeError: Expected usable value, not `true`$/, - '`true`' - ) + }, /Expected usable value, not `true`/) + } + ) - assert.throws( - () => { - // @ts-expect-error: runtime. + await t.test( + 'should throw when given invalid values (`string`)', + async function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles `string`s. unified().use('alfred') - }, - /^TypeError: Expected usable value, not `alfred`$/, - '`string`' - ) + }, /Expected usable value, not `alfred`/) + } + ) + + await t.test('should support a plugin', function () { + const processor = unified() + let called = false + + processor + .use(function () { + assert.equal(this, processor) + assert.equal(arguments.length, 0) + called = true + }) + .freeze() + + assert(called) }) - await t.test('should support plugin and options', () => { + await t.test('should support a plugin w/ options', function () { const processor = unified() - const givenOptions = {} + let called = false processor .use(function (options) { - assert.equal( - this, - processor, - 'should call a plugin with `processor` as the context' - ) - assert.equal( - options, - givenOptions, - 'should call a plugin with `options`' - ) + assert.equal(this, processor) + assert.equal(options, givenOptions) + assert.equal(arguments.length, 1) + called = true }, givenOptions) .freeze() + + assert(called) }) - await t.test('should support a list of plugins', () => { + await t.test('should support a list of plugins', function () { const processor = unified() + let calls = 0 processor .use([ function () { - assert.equal(this, processor, 'should support a list of plugins (#1)') + assert.equal(this, processor) + assert.equal(arguments.length, 0) + calls++ }, + // Note: see if we can remove this line? If we remove the previous `arguments.length` assertion, + // TS infers the plugin fine. But with it, it thinks it’s a tuple? + /** + * @satisfies {import('unified').Plugin<[]>} + * @this {import('unified').Processor} + */ function () { - assert.equal(this, processor, 'should support a list of plugins (#2)') + assert.equal(this, processor) + assert.equal(arguments.length, 0) + calls++ } ]) .freeze() + + assert.equal(calls, 2) }) - await t.test('should support a list of one plugin', () => { + await t.test('should support a list w/ a single plugin', function () { const processor = unified() + let called = false processor .use([ function () { - assert.equal(this, processor, 'should support a list of plugins (#2)') + assert.equal(this, processor) + assert.equal(arguments.length, 0) + called = true } ]) .freeze() + + assert(called) }) - await t.test('should support a list of plugins and arguments', () => { + await t.test('should support a list of tuples and plugins', function () { const processor = unified() - const givenOptions = {} + let calls = 0 processor .use([ [ - /** @param {unknown} options */ + /** + * @param {unknown} options + */ function (options) { - assert.equal( - options, - givenOptions, - 'should support arguments with options' - ) + assert.equal(options, givenOptions) + calls++ }, givenOptions ], [ function () { - assert.equal( - this, - processor, - 'should support a arguments without options' - ) + calls++ } ] ]) .freeze() - }) - - await t.test('should throw when given invalid values in lists', () => { - assert.throws( - () => { - // @ts-expect-error: runtime. - unified().use([false]) - }, - /^TypeError: Expected usable value, not `false`$/, - '`false`' - ) - - assert.throws( - () => { - // @ts-expect-error: runtime. - unified().use([true]) - }, - /^TypeError: Expected usable value, not `true`$/, - '`true`' - ) - assert.throws( - () => { - // @ts-expect-error: runtime. - unified().use(['alfred']) - }, - /^TypeError: Expected usable value, not `alfred`$/, - '`string`' - ) + assert.equal(calls, 2) }) - await t.test('should reconfigure objects', () => { - const leftOptions = {foo: true, bar: true} - const rightOptions = {foo: false, qux: true} - - unified().use(change, 'this').use(change, rightOptions).freeze() - unified().use(change).use(change, rightOptions).freeze() - unified().use(change, [1, 2, 3]).use(change, rightOptions).freeze() - unified().use(merge, leftOptions).use(merge, rightOptions).freeze() - - /** @param {unknown} [options] */ - function change(options) { - assert.deepEqual( - options, - {foo: false, qux: true}, - 'should reconfigure (set)' - ) - } - - /** @param {Record} options */ - function merge(options) { - assert.deepEqual( - options, - {foo: false, bar: true, qux: true}, - 'should reconfigure (merge)' - ) + await t.test( + 'should throw when given invalid values (`false`) in lists', + function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles `false`. + unified().use([false]) + }, /Expected usable value, not `false`/) } - }) - - await t.test('should reconfigure strings', () => { - unified().use(plugin, 'this').use(plugin, 'that').freeze() - unified().use(plugin).use(plugin, 'that').freeze() - unified().use(plugin, [1, 2, 3]).use(plugin, 'that').freeze() - unified().use(plugin, {foo: 'bar'}).use(plugin, 'that').freeze() + ) - /** @param {unknown} [options] */ - function plugin(options) { - assert.equal(options, 'that', 'should reconfigure') + await t.test( + 'should throw when given invalid values (`true`) in lists', + async function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles `true`. + unified().use([true]) + }, /Expected usable value, not `true`/) } - }) - - await t.test('should reconfigure arrays', () => { - unified().use(plugin, [1, 2, 3]).use(plugin, [4, 5, 6]).freeze() - unified().use(plugin).use(plugin, [4, 5, 6]).freeze() - unified().use(plugin, {foo: 'true'}).use(plugin, [4, 5, 6]).freeze() - unified().use(plugin, 'foo').use(plugin, [4, 5, 6]).freeze() + ) - /** @param {unknown} [options] */ - function plugin(options) { - assert.deepEqual(options, [4, 5, 6], 'should reconfigure') + await t.test( + 'should throw when given invalid values (`string`) in lists', + async function () { + assert.throws(function () { + // @ts-expect-error: check how the runtime handles `string`. + unified().use(['alfred']) + }, /Expected usable value, not `alfred`/) } - }) - - await t.test('should reconfigure to turn off', () => { - const processor = unified() - - assert.doesNotThrow(() => { - processor.use([[plugin], [plugin, false]]).freeze() - - function plugin() { - throw new Error('Error') - } - }) - }) + ) - await t.test('should reconfigure to turn on (boolean)', () => { + await t.test('should attach transformers', function () { const processor = unified() let called = false processor - .use([ - [plugin, false], - [plugin, true] - ]) + .use(function () { + return function () { + called = true + } + }) .freeze() - assert.ok(called, 'should reconfigure') + processor.runSync({type: 'test'}) - function plugin() { - called = true - } + assert.equal(called, true) }) - await t.test('should reconfigure to turn on (options)', () => { - const processor = unified() - - processor - .use([ - [plugin, false], - [plugin, {foo: true}] - ]) - .freeze() - - /** @param {unknown} [options] */ - function plugin(options) { - assert.deepEqual(options, {foo: true}, 'should reconfigure') + await t.test( + 'should throw when given a preset w/ invalid `plugins` (`false`)', + async function () { + assert.throws(function () { + unified().use({ + // @ts-expect-error: check how invalid `plugins` is handled. + plugins: false + }) + }, /Expected a list of plugins, not `false`/) } - }) - - await t.test('should attach transformers', () => { - const processor = unified() - const givenNode = {type: 'test'} - const condition = true - - processor - .use( - () => - function (node, file) { - assert.equal(node, givenNode, 'should attach a transformer (#1)') - assert.ok('message' in file, 'should attach a transformer (#2)') - - if (condition) { - throw new Error('Alpha bravo charlie') - } - } - ) - .freeze() - - assert.throws( - () => { - processor.runSync(givenNode) - }, - /Error: Alpha bravo charlie/, - 'should attach a transformer (#3)' - ) - }) -}) - -test('use(preset)', async (t) => { - assert.throws( - () => { - // @ts-expect-error: runtime. - unified().use({plugins: false}) - }, - /^TypeError: Expected a list of plugins, not `false`$/, - 'should throw on invalid `plugins` (1)' ) - assert.throws( - () => { - // @ts-expect-error: runtime. - unified().use({plugins: {foo: true}}) - }, - /^TypeError: Expected a list of plugins, not `\[object Object]`$/, - 'should throw on invalid `plugins` (2)' + await t.test( + 'should throw when given a preset w/ invalid `plugins` (`object`)', + async function () { + assert.throws(function () { + // @ts-expect-error: check how invalid `plugins` is handled. + unified().use({plugins: {foo: true}}) + }, /Expected a list of plugins, not `\[object Object]`/) + } ) - assert.throws( - () => { - unified().use({}).freeze() - }, - /Expected usable value but received an empty preset/, - 'should throw on empty presets' + await t.test( + 'should throw when given a preset w/o `settings` or `plugins`', + async function () { + assert.throws(function () { + unified().use({}).freeze() + }, /Expected usable value but received an empty preset/) + } ) - await t.test('should support presets with empty plugins', () => { + await t.test('should support a preset w/ empty `plugins`', function () { const processor = unified().use({plugins: []}).freeze() assert.equal(processor.attachers.length, 0) }) - await t.test('should support presets with empty settings', () => { + await t.test('should support a preset w/ empty `settings`', function () { const processor = unified().use({settings: {}}).freeze() assert.deepEqual(processor.data(), {settings: {}}) }) - await t.test('should support presets with a plugin', () => { + await t.test('should support a preset w/ a plugin', function () { let called = false const processor = unified() .use({plugins: [plugin]}) @@ -326,7 +262,7 @@ test('use(preset)', async (t) => { } }) - await t.test('should support presets with plugins', () => { + await t.test('should support a preset w/ plugins', function () { let calls = 0 const processor = unified() .use({plugins: [plugin1, plugin2]}) @@ -344,32 +280,36 @@ test('use(preset)', async (t) => { } }) - await t.test('should support presets with settings', () => { - const processor = unified() - .use({settings: {foo: true}}) - .freeze() - assert.deepEqual(processor.data('settings'), {foo: true}) + await t.test('should support a preset w/ settings', function () { + assert.deepEqual( + unified() + .use({settings: {foo: true}}) + .freeze() + .data(), + {settings: {foo: true}} + ) }) - await t.test('should merge multiple presets with settings', () => { - const data = unified() - .use({settings: {foo: true, bar: true}}) - .use({settings: {qux: true, foo: false}}) - .data() - - assert.deepEqual(data.settings, {foo: false, bar: true, qux: true}) + await t.test('should support presets w/ settings and merge', function () { + assert.deepEqual( + unified() + .use({settings: {foo: true, bar: true}}) + .use({settings: {qux: true, foo: false}}) + .data(), + {settings: {foo: false, bar: true, qux: true}} + ) }) - await t.test('should support extending presets', () => { + await t.test('should support extending presets', function () { let calls = 0 const processor = unified() .use({settings: {alpha: true}, plugins: [plugin1, plugin2]}) .freeze() const otherProcessor = processor().freeze() - assert.equal(processor.attachers.length, 2, '1') - assert.equal(otherProcessor.attachers.length, 2, '2') - assert.deepEqual(otherProcessor.data('settings'), {alpha: true}, '3') + assert.equal(processor.attachers.length, 2) + assert.equal(otherProcessor.attachers.length, 2) + assert.deepEqual(otherProcessor.data(), {settings: {alpha: true}}) assert.equal(calls, 4) function plugin1() { @@ -381,7 +321,7 @@ test('use(preset)', async (t) => { } }) - await t.test('should support presets with plugins as a matrix', () => { + await t.test('should support a preset w/ plugin tuples', function () { const one = {} const two = {} const processor = unified() @@ -394,25 +334,25 @@ test('use(preset)', async (t) => { .freeze() const otherProcessor = processor().freeze() - assert.equal(processor.attachers.length, 2, '1') - assert.equal(otherProcessor.attachers.length, 2, '2') + assert.equal(processor.attachers.length, 2) + assert.equal(otherProcessor.attachers.length, 2) /** * @param {unknown} options */ function plugin1(options) { - assert.equal(options, one, 'a') + assert.equal(options, one) } /** * @param {unknown} options */ function plugin2(options) { - assert.equal(options, two, 'b') + assert.equal(options, two) } }) - await t.test('should support nested presets', () => { + await t.test('should support presets w/ presets', function () { const one = {} const two = {} const processor = unified() @@ -422,17 +362,947 @@ test('use(preset)', async (t) => { .freeze() const otherProcessor = processor().freeze() - assert.equal(processor.attachers.length, 2, '1') - assert.equal(otherProcessor.attachers.length, 2, '2') + assert.equal(processor.attachers.length, 2) + assert.equal(otherProcessor.attachers.length, 2) - /** @param {unknown} [options] */ + /** + * @param {unknown} options + */ function plugin1(options) { - assert.equal(options, one, 'a') + assert.equal(options, one) } - /** @param {unknown} [options] */ + /** + * @param {unknown} options + */ function plugin2(options) { - assert.equal(options, two, 'b') + assert.equal(options, two) } }) + + await t.test('reconfigure (to `object`)', async function (t) { + await t.test( + 'should reconfigure plugins: nothing -> `object`, right wins', + async function () { + let calls = 0 + + unified().use(change).use(change, givenOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} [options] + */ + function change(options) { + assert.equal(options, givenOptions) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `undefined` -> `object`, right wins', + async function () { + let calls = 0 + + unified().use(change, undefined).use(change, givenOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptions) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `null` -> `object`, right wins', + async function () { + let calls = 0 + + unified().use(change, null).use(change, givenOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptions) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `false` -> `object`, right wins', + async function () { + let calls = 0 + + unified().use(change, false).use(change, givenOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptions) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `true` -> `object`, right wins', + async function () { + let calls = 0 + + unified().use(change, true).use(change, givenOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptions) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `string` -> `object`, right wins', + async function () { + let calls = 0 + + unified().use(change, 'this').use(change, givenOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptions) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `array` -> `object`, right wins', + async function () { + let calls = 0 + + unified() + .use(change, givenOptionsList) + .use(change, givenOptions) + .freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptions) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `object` -> `object`, merge', + async function () { + let calls = 0 + + unified().use(change, givenOptions).use(change, otherOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + // Deep, not strict, equal expected. + assert.deepEqual(options, mergedOptions) + calls++ + } + } + ) + }) + + await t.test('reconfigure (to `array`)', async function (t) { + await t.test( + 'should reconfigure plugins: nothing -> `array`, right wins', + async function () { + let calls = 0 + + unified().use(change).use(change, givenOptionsList).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} [options] + */ + function change(options) { + assert.equal(options, givenOptionsList) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `undefined` -> `array`, right wins', + async function () { + let calls = 0 + + unified().use(change, undefined).use(change, givenOptionsList).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptionsList) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `null` -> `array`, right wins', + async function () { + let calls = 0 + + unified().use(change, null).use(change, givenOptionsList).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptionsList) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `false` -> `array`, right wins', + async function () { + let calls = 0 + + unified().use(change, false).use(change, givenOptionsList).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptionsList) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `true` -> `array`, right wins', + async function () { + let calls = 0 + + unified().use(change, true).use(change, givenOptionsList).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptionsList) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `string` -> `array`, right wins', + async function () { + let calls = 0 + + unified().use(change, 'this').use(change, givenOptionsList).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptionsList) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `array` -> `array`, right wins', + async function () { + let calls = 0 + + unified() + .use(change, givenOptionsList) + .use(change, otherOptionsList) + .freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, otherOptionsList) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `object` -> `array`, right wins', + async function () { + let calls = 0 + + unified() + .use(change, givenOptions) + .use(change, givenOptionsList) + .freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, givenOptionsList) + calls++ + } + } + ) + }) + + await t.test('reconfigure (to `string`)', async function (t) { + await t.test( + 'should reconfigure plugins: nothing -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change).use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} [options] + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `undefined` -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change, undefined).use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `null` -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change, null).use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `false` -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change, false).use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `true` -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change, true).use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `string` -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change, 'this').use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `array` -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change, givenOptionsList).use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `object` -> `string`, right wins', + async function () { + let calls = 0 + + unified().use(change, givenOptions).use(change, 'x').freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, 'x') + calls++ + } + } + ) + }) + + await t.test('reconfigure (to `undefined`)', async function (t) { + await t.test( + 'should reconfigure plugins: nothing -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change).use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} [options] + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `undefined` -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change, undefined).use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `null` -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change, null).use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `false` -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change, false).use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `true` -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change, true).use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `string` -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change, 'this').use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `array` -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change, givenOptionsList).use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `object` -> `undefined`, right wins', + async function () { + let calls = 0 + + unified().use(change, givenOptions).use(change, undefined).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + }) + + await t.test('reconfigure (to `true`, to turn on)', async function (t) { + await t.test( + 'should reconfigure plugins: nothing -> `true`, used', + async function () { + let calls = 0 + + unified().use(change).use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} [options] + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `undefined` -> `true`, used', + async function () { + let calls = 0 + + unified().use(change, undefined).use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `null` -> `true`, used', + async function () { + let calls = 0 + + unified().use(change, null).use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `false` -> `true`, used', + async function () { + let calls = 0 + + unified().use(change, false).use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `true` -> `true`, used', + async function () { + let calls = 0 + + unified().use(change, true).use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `string` -> `true`, used', + async function () { + let calls = 0 + + unified().use(change, 'this').use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `array` -> `true`, used', + async function () { + let calls = 0 + + unified().use(change, givenOptionsList).use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `object` -> `true`, used', + async function () { + let calls = 0 + + unified().use(change, givenOptions).use(change, true).freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} options + */ + function change(options) { + assert.equal(options, undefined) + calls++ + } + } + ) + }) + + await t.test('reconfigure (to `false`, to turn off)', async function (t) { + await t.test( + 'should reconfigure plugins: nothing -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change).use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `undefined` -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change, undefined).use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `null` -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change, null).use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `false` -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change, false).use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `true` -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change, true).use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `string` -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change, 'this').use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `array` -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change, givenOptionsList).use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + + await t.test( + 'should reconfigure plugins: `object` -> `false`, not used', + async function () { + let calls = 0 + + unified().use(change, givenOptions).use(change, false).freeze() + + assert.equal(calls, 0) + + /** + * @param {unknown} [_] + */ + function change(_) { + calls++ + } + } + ) + }) }) diff --git a/test/util/simple.js b/test/util/simple.js index 64c12e5f..41abd306 100644 --- a/test/util/simple.js +++ b/test/util/simple.js @@ -1,9 +1,12 @@ /** - * @typedef {import('unist').Literal} Literal * @typedef {import('unified').Parser} Parser * @typedef {import('unified').Compiler} Compiler + * @typedef {import('unist').Literal} Literal */ +// Make references to the above types visible in VS Code. +'' + /** @type {Parser} */ export class SimpleParser { /** @param {string} doc */ From fb49556512b4bc4fde317cda66937cb289a0715e Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 10 Aug 2023 16:44:03 +0200 Subject: [PATCH 29/46] Change to replace `Buffer` with `Uint8Array` Previously, `VFile` supported `Buffer`, which is Node.js-specific. It changed to `Uint8Array`. In most cases, this will be fine, because the Node `Buffer` class subclasses `Uint8Array`. Related-to: vfile/vfile@f4edd0d. --- index.d.ts | 28 ++++++++++++++-------------- index.test-d.ts | 29 +++++++++++++++++------------ lib/index.js | 20 ++++++++++++++++++-- package.json | 1 - readme.md | 10 +++++----- test/process-compilers.js | 5 ++--- 6 files changed, 56 insertions(+), 37 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6f881a87..4b0f61cc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -111,7 +111,7 @@ export type Processor< * * If the plugin sets a parser, then this should be the node type that * the parser yields. * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Buffer`, or something else). + * the compiler yields (`string`, `Uint8Array`, or something else). * @param plugin * Plugin (function) to use. * Plugins are deduped based on identity: passing a function in twice will @@ -162,7 +162,7 @@ export type Processor< * * If the plugin sets a parser, then this should be the node type that * the parser yields. * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Buffer`, or something else). + * the compiler yields (`string`, `Uint8Array`, or something else). * @param tuple * A tuple where the first item is a plugin (function) to use and other * items are options. @@ -260,7 +260,7 @@ export type FrozenProcessor< * @param file * `VFile` or anything that can be given to `new VFile()`, optional. * @returns - * New content: compiled text (`string` or `Buffer`) or something else. + * New content: compiled text (`string` or `Uint8Array`) or something else. * This depends on which plugins you use: typically text, but could for * example be a React node. */ @@ -347,8 +347,8 @@ export type FrozenProcessor< * * The result from the compiler is stored on the file. * What the result is depends on which plugins you use. - * The result is typically text (`string` or `Buffer`), which can be retrieved - * with `file.toString()` (or `String(file)`). + * The result is typically text (`string` or `Uint8Array`), which can be + * retrieved with `file.toString()` (or `String(file)`). * In some cases, such as when using `rehypeReact` to create a React node, * the result is stored on `file.result`. * @@ -375,8 +375,8 @@ export type FrozenProcessor< * * The result from the compiler is stored on the file. * What the result is depends on which plugins you use. - * The result is typically text (`string` or `Buffer`), which can be retrieved - * with `file.toString()` (or `String(file)`). + * The result is typically text (`string` or `Uint8Array`), which can be + * retrieved with `file.toString()` (or `String(file)`). * In some cases, such as when using `rehypeReact` to create a React node, * the result is stored on `file.result`. * @@ -399,8 +399,8 @@ export type FrozenProcessor< * * The result from the compiler is stored on the file. * What the result is depends on which plugins you use. - * The result is typically text (`string` or `Buffer`), which can be retrieved - * with `file.toString()` (or `String(file)`). + * The result is typically text (`string` or `Uint8Array`), which can be + * retrieved with `file.toString()` (or `String(file)`). * In some cases, such as when using `rehypeReact` to create a React node, * the result is stored on `file.result`. * @@ -502,7 +502,7 @@ export type FrozenProcessor< * * If the plugin sets a parser, then this should be the node type that * the parser yields. * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Buffer`, or something else). + * the compiler yields (`string`, `Uint8Array`, or something else). * @this * The current processor. * Plugins can configure the processor by interacting with `this.Parser` or @@ -580,7 +580,7 @@ export type Preset = { * * If the plugin sets a parser, then this should be the node type that * the parser yields. * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Buffer`, or something else). + * the compiler yields (`string`, `Uint8Array`, or something else). */ export type PluginTuple< PluginParameters extends any[] = any[], @@ -780,8 +780,8 @@ export class CompilerClass { * Compile a tree. * * @returns - * New content: compiled text (`string` or `Buffer`, for `file.value`) or - * something else (for `file.result`). + * New content: compiled text (`string` or `Uint8Array`, for + * `file.value`) or something else (for `file.result`). */ compile(): Result } @@ -811,7 +811,7 @@ export class CompilerClass { * @param file * File associated with `tree`. * @returns - * New content: compiled text (`string` or `Buffer`, for `file.value`) or + * New content: compiled text (`string` or `Uint8Array`, for `file.value`) or * something else (for `file.result`). */ export type CompilerFunction = ( diff --git a/index.test-d.ts b/index.test-d.ts index 9d71acb0..b51f9fb1 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -524,27 +524,32 @@ expectType(processorWithRehypeStringify.stringify(hastRoot)) processorWithRehypeStringify.stringify(mdastRoot) expectType(processorWithRehypeStringify.processSync('')) -// Compile plugin (to a buffer). -const rehypeStringifyBuffer: Plugin<[], HastRoot, Uint8Array> = function () { - // Empty. -} +// Compile plugin (to an `Uint8Array`). +const rehypeStringifyUint8Array: Plugin<[], HastRoot, Uint8Array> = + function () { + // Empty. + } -const processorWithRehypeStringifyBuffer = unified().use(rehypeStringifyBuffer) +const processorWithRehypeStringifyUint8Array = unified().use( + rehypeStringifyUint8Array +) // To do: ? expectType>( - processorWithRehypeStringifyBuffer + processorWithRehypeStringifyUint8Array ) // To do: yield `UnistNode`? -expectType(processorWithRehypeStringifyBuffer.parse('')) +expectType(processorWithRehypeStringifyUint8Array.parse('')) // @ts-expect-error: to do: accept `UnistNode`? -processorWithRehypeStringifyBuffer.runSync(mdastRoot) +processorWithRehypeStringifyUint8Array.runSync(mdastRoot) // To do: accept, yield `UnistNode`? -expectType(processorWithRehypeStringifyBuffer.runSync(hastRoot)) -expectType(processorWithRehypeStringifyBuffer.stringify(hastRoot)) +expectType(processorWithRehypeStringifyUint8Array.runSync(hastRoot)) +expectType( + processorWithRehypeStringifyUint8Array.stringify(hastRoot) +) // @ts-expect-error: not the correct node type. -processorWithRehypeStringifyBuffer.stringify(mdastRoot) -expectType(processorWithRehypeStringifyBuffer.processSync('')) +processorWithRehypeStringifyUint8Array.stringify(mdastRoot) +expectType(processorWithRehypeStringifyUint8Array.processSync('')) // Compile plugin (to a non-node). const rehypeReact: Plugin<[], HastRoot, ReactNode> = function () { diff --git a/lib/index.js b/lib/index.js index f02abd1a..3516154c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -18,7 +18,6 @@ import structuredClone from '@ungap/structured-clone' import {bail} from 'bail' -import isBuffer from 'is-buffer' import isPlainObj from 'is-plain-obj' import {trough} from 'trough' import {VFile} from 'vfile' @@ -601,5 +600,22 @@ function looksLikeAVFile(value) { * @returns {value is VFileValue} */ function looksLikeAVFileValue(value) { - return typeof value === 'string' || isBuffer(value) + return typeof value === 'string' || isUint8Array(value) +} + +/** + * Assert `value` is an `Uint8Array`. + * + * @param {unknown} value + * thing. + * @returns {value is Uint8Array} + * Whether `value` is an `Uint8Array`. + */ +function isUint8Array(value) { + return Boolean( + value && + typeof value === 'object' && + 'byteLength' in value && + 'byteOffset' in value + ) } diff --git a/package.json b/package.json index 3135cd1e..b448ae3d 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "bail": "^2.0.0", - "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" diff --git a/readme.md b/readme.md index c3c8e53c..003a4ad4 100644 --- a/readme.md +++ b/readme.md @@ -526,10 +526,10 @@ Compile a syntax tree. ###### Returns -Textual representation of the tree (`string` or `Buffer`, see note). +Textual representation of the tree (`string` or `Uint8Array`, see note). > 👉 **Note**: unified typically compiles by serializing: most -> [compilers][compiler] return `string` (or `Buffer`). +> [compilers][compiler] return `string` (or `Uint8Array`). > Some compilers, such as the one configured with > [`rehype-react`][rehype-react], return other values (in this case, a React > tree). @@ -575,7 +575,7 @@ Instances must have a `compile` method that is called without arguments and should return a `string`. > 👉 **Note**: unified typically compiles by serializing: most compilers -> return `string` (or `Buffer`). +> return `string` (or `Uint8Array`). > Some compilers, such as the one configured with > [`rehype-react`][rehype-react], return other values (in this case, a React > tree). @@ -693,7 +693,7 @@ The parsed, transformed, and compiled value is available at [`file.value`][vfile-value] (see note). > 👉 **Note**: unified typically compiles by serializing: most -> [compilers][compiler] return `string` (or `Buffer`). +> [compilers][compiler] return `string` (or `Uint8Array`). > Some compilers, such as the one configured with > [`rehype-react`][rehype-react], result in other values (in this case, a React > tree). @@ -805,7 +805,7 @@ The parsed, transformed, and compiled value is available at [`file.value`][vfile-value] (see note). > 👉 **Note**: unified typically compiles by serializing: most -> [compilers][compiler] return `string` (or `Buffer`). +> [compilers][compiler] return `string` (or `Uint8Array`). > Some compilers, such as the one configured with > [`rehype-react`][rehype-react], result in other values (in this case, a React > tree). diff --git a/test/process-compilers.js b/test/process-compilers.js index 46285de5..c71a4093 100644 --- a/test/process-compilers.js +++ b/test/process-compilers.js @@ -1,4 +1,3 @@ -import {Buffer} from 'node:buffer' import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' @@ -20,9 +19,9 @@ test('process (compilers)', async function (t) { assert.equal(file.result, undefined) }) - await t.test('should compile `buffer`', async function () { + await t.test('should compile `Uint8Array`', async function () { const processor = unified() - const result = Buffer.from('bravo') + const result = new Uint8Array([0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63]) processor.Parser = SimpleParser processor.Compiler = function () { From 1aa3494da4c98b0a1ce4d112c653d86aaa21a4ae Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 10 Aug 2023 16:47:22 +0200 Subject: [PATCH 30/46] Change to yield `undefined`, not `null` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You can still pass it, just that *we* don’t. --- index.d.ts | 6 +++--- lib/index.js | 26 +++++++++++++------------- test/data.js | 4 ++-- test/run.js | 20 ++++++++++---------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4b0f61cc..20c00835 100644 --- a/index.d.ts +++ b/index.d.ts @@ -672,7 +672,7 @@ export type Transformer< * Nothing. */ export type TransformCallback = ( - error?: Error | null | undefined, + error?: Error | undefined, node?: Tree | undefined, file?: VFile | undefined ) => void @@ -834,7 +834,7 @@ export type CompilerFunction = ( * Nothing. */ export type RunCallback = ( - error?: Error | null | undefined, + error?: Error | undefined, node?: Tree | undefined, file?: VFile | undefined ) => void @@ -852,7 +852,7 @@ export type RunCallback = ( * Nothing. */ export type ProcessCallback = ( - error?: Error | null | undefined, + error?: Error | undefined, file?: File | undefined ) => void diff --git a/lib/index.js b/lib/index.js index 3516154c..670d6fe0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -98,7 +98,7 @@ function base() { } // Get `key`. - return (own.call(namespace, key) && namespace[key]) || null + return (own.call(namespace, key) && namespace[key]) || undefined } // Set space. @@ -314,10 +314,10 @@ function base() { return new Promise(executor) } - executor(null, callback) + executor(undefined, callback) /** - * @param {((node: Node) => void) | null} resolve + * @param {((node: Node) => void) | undefined} resolve * @param {(error: Error) => void} reject * @returns {void} */ @@ -326,7 +326,7 @@ function base() { transformers.run(node, vfile(doc), done) /** - * @param {Error | null} error + * @param {Error | undefined} error * @param {Node} tree * @param {VFile} file * @returns {void} @@ -339,7 +339,7 @@ function base() { resolve(tree) } else { // @ts-expect-error: `callback` is defined if `resolve` is not. - callback(null, tree, file) + callback(undefined, tree, file) } } } @@ -360,7 +360,7 @@ function base() { return result /** - * @param {Error | null} [error] + * @param {Error | undefined} [error] * @param {Node} [tree] * @returns {void} */ @@ -385,11 +385,11 @@ function base() { return new Promise(executor) } - executor(null, callback) + executor(undefined, callback) /** - * @param {((file: VFile) => void) | null} resolve - * @param {(error?: Error | null | undefined) => void} reject + * @param {((file: VFile) => void) | undefined} resolve + * @param {(error?: Error | undefined) => void} reject * @returns {void} */ function executor(resolve, reject) { @@ -402,7 +402,7 @@ function base() { /** @type {unknown} */ const result = processor.stringify(tree, file) - if (result === undefined || result === null) { + if (result === null || result === undefined) { // Empty. } else if (looksLikeAVFileValue(result)) { file.value = result @@ -415,7 +415,7 @@ function base() { }) /** - * @param {Error | null | undefined} [error] + * @param {Error | undefined} [error] * @param {VFile | undefined} [file] * @returns {void} */ @@ -426,7 +426,7 @@ function base() { resolve(file) } else { // @ts-expect-error: `callback` is defined if `resolve` is not. - callback(null, file) + callback(undefined, file) } } } @@ -450,7 +450,7 @@ function base() { return file /** - * @param {Error | null | undefined} [error] + * @param {Error | undefined} [error] * @returns {void} */ function done(error) { diff --git a/test/data.js b/test/data.js index e51a786c..671bdaab 100644 --- a/test/data.js +++ b/test/data.js @@ -10,7 +10,7 @@ test('`data`', async function (t) { }) await t.test('should yield data as getter (not defined)', async function () { - assert.equal(unified().data('foo'), null) + assert.equal(unified().data('foo'), undefined) }) await t.test('should yield data as getter (defined)', async function () { @@ -18,7 +18,7 @@ test('`data`', async function (t) { }) await t.test('should not yield data prototypal fields', async function () { - assert.equal(unified().data('toString'), null) + assert.equal(unified().data('toString'), undefined) }) await t.test('should yield dataset as getter w/o key', async function () { diff --git a/test/run.js b/test/run.js index c4caad54..9a8fb1c0 100644 --- a/test/run.js +++ b/test/run.js @@ -12,7 +12,7 @@ test('`run`', async function (t) { await t.test('should pass/yield expected values', async function () { await new Promise(function (resolve) { unified().run(givenNode, givenFile, function (error, tree, file) { - assert.equal(error, null) + assert.equal(error, undefined) assert.equal(tree, givenNode) assert.equal(file, givenFile) assert.equal(arguments.length, 3) @@ -24,7 +24,7 @@ test('`run`', async function (t) { await t.test('should pass a file if implicitly not given', async function () { await new Promise(function (resolve) { unified().run(givenNode, function (error, _, file) { - assert.equal(error, null) + assert.equal(error, undefined) assert.ok(file instanceof VFile) resolve(undefined) }) @@ -34,7 +34,7 @@ test('`run`', async function (t) { await t.test('should pass a file if explicitly not given', async function () { await new Promise(function (resolve) { unified().run(givenNode, undefined, function (error, _, file) { - assert.equal(error, null) + assert.equal(error, undefined) assert.ok(file instanceof VFile) resolve(undefined) }) @@ -70,7 +70,7 @@ test('`run`', async function (t) { } }) .run(givenNode, function (error, tree) { - assert.equal(error, null) + assert.equal(error, undefined) assert.equal(tree, otherNode) resolve(undefined) }) @@ -128,7 +128,7 @@ test('`run`', async function (t) { } }) .run(givenNode, function (error, tree) { - assert.equal(error, null) + assert.equal(error, undefined) assert.equal(tree, otherNode) resolve(undefined) }) @@ -150,7 +150,7 @@ test('`run`', async function (t) { } }) .run(givenNode, function (error, tree) { - assert.equal(error, null) + assert.equal(error, undefined) assert.equal(tree, otherNode) resolve(undefined) }) @@ -191,7 +191,7 @@ test('`run`', async function (t) { } }) .run(givenNode, function (error, tree) { - assert.equal(error, null) + assert.equal(error, undefined) assert.equal(tree, otherNode) resolve(undefined) }) @@ -212,7 +212,7 @@ test('`run`', async function (t) { } }) .run(givenNode, function (error) { - assert.equal(error, null) + assert.equal(error, undefined) resolve(undefined) }) }) @@ -239,7 +239,7 @@ test('`run`', async function (t) { } }) .run(givenNode, function (error) { - assert.equal(error, null) + assert.equal(error, undefined) resolve(undefined) }) }) @@ -277,7 +277,7 @@ test('`run`', async function (t) { } }) .run(givenNode, givenFile, function (error, tree, file) { - assert.equal(error, null) + assert.equal(error, undefined) assert.equal(tree, otherNode) assert.equal(file, givenFile) resolve(undefined) From 807ffb97929aa57b1036d27e31413d3ee5670767 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sat, 12 Aug 2023 16:59:26 +0200 Subject: [PATCH 31/46] Add improved types * Add better support for different compile results, if you have custom results, add them to `CompileResultMap` * Better input/output of functions * Infer plugins better * Redo all docs --- index.d.ts | 1162 +++++++++++++++++++++++-------------- index.test-d.ts | 262 ++++----- lib/index.js | 48 +- package.json | 7 +- readme.md | 41 +- test/freeze.js | 52 +- test/process-compilers.js | 11 +- test/process-sync.js | 45 +- test/process.js | 12 +- test/run-sync.js | 14 +- test/run.js | 42 +- test/stringify.js | 1 + test/use.js | 18 +- test/util/simple.js | 38 +- 14 files changed, 1016 insertions(+), 737 deletions(-) diff --git a/index.d.ts b/index.d.ts index 20c00835..cf53d2af 100644 --- a/index.d.ts +++ b/index.d.ts @@ -14,127 +14,258 @@ // accept type parameters cannot be re-exported as such easily. import type {Node} from 'unist' -import type {VFile, VFileCompatible} from 'vfile' +import type {VFile, VFileCompatible, VFileValue} from 'vfile' -type VFileWithOutput = Result extends Uint8Array - ? VFile - : Result extends object // Custom result type - ? VFile & {result: Result} - : VFile +/** + * Interface of known results from compilers. + * + * Normally, compilers result in text ({@link VFileValue `VFileValue`}). + * When you compile to something else, such as a React node (as in, + * `rehype-react`), you can augment this interface to include that type. + * + * ```ts + * import type {ReactNode} from 'somewhere' + * + * declare module 'unified' { + * interface CompileResultMap { + * // Register a new result (value is used, key should match it). + * ReactNode: ReactNode + * } + * } + * ``` + * + * Use {@link CompileResults `CompileResults`} to access the values. + */ +// Note: if `Value` from `VFile` is changed, this should too. +export interface CompileResultMap { + Uint8Array: Uint8Array + string: string +} -// Get the right most non-void thing. -type Specific = Right extends undefined | void - ? Left - : Right +/** + * Acceptable results from compilers. + * + * To register custom results, add them to + * {@link CompileResultMap `CompileResultMap`}. + */ +type CompileResults = CompileResultMap[keyof CompileResultMap] -// Create a processor based on the input/output of a plugin. +/** + * Type to generate a {@link VFile `VFile`} corresponding to a compiler result. + * + * If a result that is not acceptable on a `VFile` is used, that will + * be stored on the `result` field of {@link VFile `VFile`}. + * + * @typeParam Result + * Compile result. + */ +type VFileWithOutput = + Result extends VFileValue | undefined ? VFile : VFile & {result: Result} + +/** + * Create a processor based on the input/output of a {@link Plugin plugin}. + * + * @typeParam ParseTree + * Output of `parse`. + * @typeParam HeadTree + * Input for `run`. + * @typeParam TailTree + * Output for `run`. + * @typeParam CompileTree + * Input of `stringify`. + * @typeParam CompileResult + * Output of `stringify`. + * @typeParam Input + * Input of plugin. + * @typeParam Output + * Output of plugin. + */ type UsePlugin< - ParseTree extends Node | void = void, - CurrentTree extends Node | void = void, - CompileTree extends Node | void = void, - CompileResult = void, - Input = void, - Output = void -> = Output extends Node - ? Input extends string - ? // If `Input` is `string` and `Output` is `Node`, then this plugin - // defines a parser, so set `ParseTree`. + ParseTree extends Node | undefined, + HeadTree extends Node | undefined, + TailTree extends Node | undefined, + CompileTree extends Node | undefined, + CompileResult extends CompileResults | undefined, + Input extends Node | string | undefined, + Output +> = Input extends string + ? Output extends Node | undefined + ? // Parser. Processor< - Output, - Specific, - Specific, + Output extends undefined ? ParseTree : Output, + HeadTree, + TailTree, + CompileTree, CompileResult > - : Input extends Node - ? // If `Input` is `Node` and `Output` is `Node`, then this plugin defines a - // transformer, its output defines the input of the next, so set - // `CurrentTree`. + : // Unknown. + Processor + : Output extends CompileResults + ? Input extends Node | undefined + ? // Compiler. + Processor< + ParseTree, + HeadTree, + TailTree, + Input extends undefined ? CompileTree : Input, + Output extends undefined ? CompileResult : Output + > + : // Unknown. + Processor + : Input extends Node | undefined + ? Output extends Node | undefined + ? // Transform. Processor< - Specific, - Output, - Specific, + ParseTree, + // No `HeadTree` yet? Set `Input`. + HeadTree extends undefined ? Input : HeadTree, + Output extends undefined ? TailTree : Output, + CompileTree, CompileResult > - : // Else, `Input` is something else and `Output` is `Node`: - never - : Input extends Node - ? // If `Input` is `Node` and `Output` is not a `Node`, then this plugin - // defines a compiler, so set `CompileTree` and `CompileResult` - Processor< - Specific, - Specific, - Input, - Output - > - : // Else, `Input` is not a `Node` and `Output` is not a `Node`. - // Maybe it’s untyped, or the plugin throws an error (`never`), so lets - // just keep it as it was. - Processor + : // Unknown. + Processor + : // Unknown. + Processor /** - * Processor allows plugins to be chained together to transform content. - * The chain of plugins defines how content flows through it. + * Processor. * * @typeParam ParseTree - * The node that the parser yields (and `run` receives). - * @typeParam CurrentTree - * The node that the last attached plugin yields. + * Output of `parse`. + * @typeParam HeadTree + * Input for `run`. + * @typeParam TailTree + * Output for `run`. * @typeParam CompileTree - * The node that the compiler receives (and `run` yields). + * Input of `stringify`. * @typeParam CompileResult - * The thing that the compiler yields. + * Output of `stringify`. */ export type Processor< - ParseTree extends Node | void = void, - CurrentTree extends Node | void = void, - CompileTree extends Node | void = void, - CompileResult = void + ParseTree extends Node | undefined = undefined, + HeadTree extends Node | undefined = undefined, + TailTree extends Node | undefined = undefined, + CompileTree extends Node | undefined = undefined, + CompileResult extends CompileResults | undefined = undefined > = { /** - * Configure the processor to use a plugin. + * Configure the processor with a preset. + * + * If the processor is already using a plugin, the previous plugin + * configuration is changed based on the options that are passed in. + * In other words, the plugin is not added a second time. + * + * @example + * ```js + * import {unified} from 'unified' * - * @typeParam PluginParameters - * Plugin settings. + * unified() + * // Preset with plugins and settings: + * .use({plugins: [pluginA, [pluginB, {}]], settings: {position: false}}) + * // Settings only: + * .use({settings: {position: false}}) + * ``` + * + * @param preset + * Single preset ({@link Preset `Preset`}): an object with a `plugins` + * and/or `settings`. + * @returns + * Current processor. + */ + use( + preset?: Preset | null | undefined + ): Processor + + /** + * Configure the processor with a list of usable values. + * + * If the processor is already using a plugin, the previous plugin + * configuration is changed based on the options that are passed in. + * In other words, the plugin is not added a second time. + * + * @example + * ```js + * import {unified} from 'unified' + * + * unified() + * // Plugins: + * .use([pluginA, pluginB]) + * // Two plugins, the second with options: + * .use([pluginC, [pluginD, {}]]) + * ``` + * + * @param list + * List of plugins plugins, presets, and tuples + * ({@link PluggableList `PluggableList`}). + * @returns + * Current processor. + */ + use( + list: PluggableList + ): Processor + + /** + * Configure the processor to use a {@link Plugin `Plugin`}. + * + * If the processor is already using a plugin, the previous plugin + * configuration is changed based on the options that are passed in. + * In other words, the plugin is not added a second time. + * + * @example + * ```js + * import {unified} from 'unified' + * + * unified() + * // Plugin with options: + * .use(pluginA, {x: true, y: true}) + * // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): + * .use(pluginA, {y: false, z: true}) + * ``` + * + * @typeParam Parameters + * Arguments passed to the plugin. * @typeParam Input - * Value that is accepted by the plugin. + * Value that is expected as input. * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer expects. - * * If the plugin sets a parser, then this should be `string`. - * * If the plugin sets a compiler, then this should be the node type that - * the compiler expects. + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node it expects. + * * If the plugin sets a {@link Parser `Parser`}, this should be + * `string`. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be the + * node it expects. * @typeParam Output - * Value that the plugin yields. - * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer yields, and defaults to `Input`. - * * If the plugin sets a parser, then this should be the node type that - * the parser yields. - * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Uint8Array`, or something else). + * Value that is yielded as output. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node that that yields. + * * If the plugin sets a {@link Parser `Parser`}, this should be the + * node that it yields. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be + * result it yields. * @param plugin - * Plugin (function) to use. - * Plugins are deduped based on identity: passing a function in twice will - * cause it to run only once. - * @param settings - * Configuration for plugin, optional. + * {@link Plugin `Plugin`} to use. + * @param parameters + * Arguments passed to the {@link Plugin plugin}. + * * Plugins typically receive one options object, but could receive other and * more values. - * It’s also possible to pass a boolean instead of settings: `true` (to turn - * a plugin on) or `false` (to turn a plugin off). + * It’s also possible to pass a boolean: `true` (to turn a plugin on), + * `false` (to turn a plugin off). * @returns * Current processor. */ use< - PluginParameters extends any[] = any[], - Input = Specific, + Parameters extends unknown[] = [], + Input extends Node | string | undefined = undefined, Output = Input >( - plugin: Plugin, - ...settings: PluginParameters | [boolean] + plugin: Plugin, + ...parameters: Parameters | [boolean] ): UsePlugin< ParseTree, - CurrentTree, + HeadTree, + TailTree, CompileTree, CompileResult, Input, @@ -142,299 +273,427 @@ export type Processor< > /** - * Configure the processor with a tuple of a plugin and setting(s). + * Configure the processor to use a tuple of a {@link Plugin `Plugin`} with + * its parameters. * - * @typeParam PluginParameters - * Plugin settings. + * If the processor is already using a plugin, the previous plugin + * configuration is changed based on the options that are passed in. + * In other words, the plugin is not added a second time. + * + * @example + * ```js + * import {unified} from 'unified' + * + * unified() + * // Plugin with options: + * .use([pluginA, {x: true, y: true}]) + * ``` + * + * @typeParam Parameters + * Arguments passed to the plugin. * @typeParam Input - * Value that is accepted by the plugin. + * Value that is expected as input. * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer expects. - * * If the plugin sets a parser, then this should be `string`. - * * If the plugin sets a compiler, then this should be the node type that - * the compiler expects. + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node it expects. + * * If the plugin sets a {@link Parser `Parser`}, this should be + * `string`. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be the + * node it expects. * @typeParam Output - * Value that the plugin yields. - * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer yields, and defaults to `Input`. - * * If the plugin sets a parser, then this should be the node type that - * the parser yields. - * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Uint8Array`, or something else). + * Value that is yielded as output. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node that that yields. + * * If the plugin sets a {@link Parser `Parser`}, this should be the + * node that it yields. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be + * result it yields. * @param tuple - * A tuple where the first item is a plugin (function) to use and other - * items are options. - * Plugins are deduped based on identity: passing a function in twice will - * cause it to run only once. - * It’s also possible to pass a boolean instead of settings: `true` (to turn - * a plugin on) or `false` (to turn a plugin off). + * {@link Plugin `Plugin`} with arguments to use. + * + * Plugins typically receive one options object, but could receive other and + * more values. + * It’s also possible to pass a boolean: `true` (to turn a plugin on), + * `false` (to turn a plugin off). * @returns * Current processor. */ use< - PluginParameters extends any[] = any[], - Input = Specific, + Parameters extends unknown[] = [], + Input extends Node | string | undefined = undefined, Output = Input >( tuple: - | PluginTuple - | [Plugin, boolean] + | [plugin: Plugin, enable: boolean] // Enable or disable the plugin. + | [plugin: Plugin, ...parameters: Parameters] // Configure the plugin. ): UsePlugin< ParseTree, - CurrentTree, + HeadTree, + TailTree, CompileTree, CompileResult, Input, Output > - - /** - * Configure the processor with a preset or list of plugins and presets. - * - * @param presetOrList - * Either a list of plugins, presets, and tuples, or a single preset: an - * object with a `plugins` (list) and/or `settings` - * (`Record`). - * @returns - * Current processor. - */ - use( - presetOrList: PluggableList | Preset - ): Processor -} & FrozenProcessor +} & FrozenProcessor /** - * A frozen processor is just like a regular processor, except no additional - * plugins can be added. - * A frozen processor can be created by calling `.freeze()` on a processor. - * An unfrozen processor can be created by calling a processor. + * Frozen processor. + * + * @typeParam ParseTree + * Output of `parse`. + * @typeParam HeadTree + * Input for `run`. + * @typeParam TailTree + * Output for `run`. + * @typeParam CompileTree + * Input of `stringify`. + * @typeParam CompileResult + * Output of `stringify`. */ export type FrozenProcessor< - ParseTree extends Node | void = void, - CurrentTree extends Node | void = void, - CompileTree extends Node | void = void, - CompileResult = void + ParseTree extends Node | undefined = undefined, + HeadTree extends Node | undefined = undefined, + TailTree extends Node | undefined = undefined, + CompileTree extends Node | undefined = undefined, + CompileResult extends CompileResults | undefined = undefined > = { /** - * Clone current processor + * Create a processor. * * @returns - * New unfrozen processor that is configured to function the same as its - * ancestor. - * But when the descendant processor is configured it does not affect the - * ancestral processor. + * New *unfrozen* processor ({@link Processor `Processor`}) that is + * configured to work the same as its ancestor. + * When the descendant processor is configured in the future it does not + * affect the ancestral processor. */ - (): Processor + (): Processor /** * Internal list of configured plugins. * * @private */ - attachers: Array<[Plugin, ...unknown[]]> + attachers: Array> - Parser?: Parser> | undefined + /** + * A **parser** handles the parsing of text to a syntax tree. + * + * It is used in the parse phase and is called with a `string` and + * {@link VFile `VFile`} of the document to parse. + * + * `Parser` can be a normal function, in which case it must return the syntax + * tree representation of the given file ({@link Node `Node`}). + * + * `Parser` can also be a constructor function (a function with a `parse` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `parse` method that is called without arguments and must + * return a {@link Node `Node`}. + */ + Parser?: Parser | undefined + /** + * A **compiler** handles the compiling of a syntax tree to something else (in + * most cases, text). + * + * It is used in the stringify phase and called with a {@link Node `Node`} + * and {@link VFile `VFile`} representation of the document to compile. + * + * `Compiler` can be a normal function, in which case it should return the + * textual representation of the given tree (`string`). + * + * `Compiler` can also be a constructor function (a function with a `compile` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `compile` method that is called without arguments and + * should return a `string`. + * + * > 👉 **Note**: unified typically compiles by serializing: most compilers + * > return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different result + * > values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react + */ Compiler?: - | Compiler, Specific> + | Compiler< + CompileTree extends undefined ? Node : CompileTree, + CompileResult extends undefined ? unknown : CompileResult + > | undefined /** - * Parse a file. + * Parse text to a syntax tree. + * + * > 👉 **Note**: `parse` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `parse` performs the parse phase, not the run phase or other + * > phases. * * @param file - * File to parse. - * `VFile` or anything that can be given to `new VFile()`, optional. + * file to parse; typically `string`; any value accepted as `x` in + * `new VFile(x)`. * @returns - * Resulting tree. + * Syntax tree representing `file`. */ - parse(file?: VFileCompatible | undefined): Specific + parse( + file?: VFileCompatible | undefined + ): ParseTree extends undefined ? Node : ParseTree /** - * Compile a file. + * Compile a syntax tree. + * + * > 👉 **Note**: `stringify` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `stringify` performs the stringify phase, not the run phase + * or other phases. * - * @param node - * Node to compile. + * @param tree + * Tree to compile * @param file - * `VFile` or anything that can be given to `new VFile()`, optional. + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. * @returns - * New content: compiled text (`string` or `Uint8Array`) or something else. - * This depends on which plugins you use: typically text, but could for - * example be a React node. + * Textual representation of the tree (see note). + * + * > 👉 **Note**: unified typically compiles by serializing: most compilers + * > return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different result + * > values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react */ stringify( - node: Specific, + tree: CompileTree extends undefined ? Node : CompileTree, file?: VFileCompatible | undefined - ): CompileTree extends Node ? CompileResult : unknown + ): CompileResult extends undefined ? VFileValue : CompileResult /** - * Run transforms on the given tree. + * Run *transformers* on a syntax tree. + * + * > 👉 **Note**: `run` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `run` performs the run phase, not other phases. * - * @param node - * Tree to transform. - * @param callback - * Callback called with an error or the resulting node. + * @param tree + * Tree to transform and inspect. + * @param done + * Callback. * @returns * Nothing. */ run( - node: Specific, - callback: RunCallback> - ): void + tree: HeadTree extends undefined ? Node : HeadTree, + done: RunCallback + ): undefined /** - * Run transforms on the given node. + * Run *transformers* on a syntax tree. + * + * > 👉 **Note**: `run` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `run` performs the run phase, not other phases. * - * @param node - * Tree to transform. + * @param tree + * Tree to transform and inspect. * @param file - * File associated with `node`. - * `VFile` or anything that can be given to `new VFile()`. - * @param callback - * Callback called with an error or the resulting node. + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. + * @param done + * Callback. * @returns * Nothing. */ run( - node: Specific, + tree: HeadTree extends undefined ? Node : HeadTree, file: VFileCompatible | undefined, - callback: RunCallback> - ): void + done: RunCallback + ): undefined /** - * Run transforms on the given node. + * Run *transformers* on a syntax tree. + * + * > 👉 **Note**: `run` freezes the processor if not already *frozen*. * - * @param node - * Tree to transform. + * > 👉 **Note**: `run` performs the run phase, not other phases. + * + * @param tree + * Tree to transform and inspect. * @param file - * File associated with `node`. - * `VFile` or anything that can be given to `new VFile()`. + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. * @returns - * Promise that resolves to the resulting tree. + * A `Promise` rejected with a fatal error or resolved with the transformed + * tree. */ run( - node: Specific, + tree: HeadTree extends undefined ? Node : HeadTree, file?: VFileCompatible | undefined - ): Promise> + ): Promise /** - * Run transforms on the given node, synchronously. - * Throws when asynchronous transforms are configured. + * Run *transformers* on a syntax tree. + * + * An error is thrown if asynchronous transforms are configured. + * + * > 👉 **Note**: `runSync` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `runSync` performs the run phase, not other phases. * - * @param node - * Tree to transform. + * @param tree + * Tree to transform and inspect. * @param file - * File associated with `node`. - * `VFile` or anything that can be given to `new VFile()`, optional. + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. * @returns - * Resulting tree. + * Transformed tree. */ runSync( - node: Specific, + tree: HeadTree extends undefined ? Node : HeadTree, file?: VFileCompatible | undefined - ): Specific + ): TailTree extends undefined ? Node : TailTree /** - * Process a file. - * - * This performs all phases of the processor: + * Process the given file as configured on the processor. * - * 1. Parse a file into a unist node using the configured `Parser` - * 2. Run transforms on that node - * 3. Compile the resulting node using the `Compiler` + * > 👉 **Note**: `process` freezes the processor if not already *frozen*. * - * The result from the compiler is stored on the file. - * What the result is depends on which plugins you use. - * The result is typically text (`string` or `Uint8Array`), which can be - * retrieved with `file.toString()` (or `String(file)`). - * In some cases, such as when using `rehypeReact` to create a React node, - * the result is stored on `file.result`. + * > 👉 **Note**: `process` performs the parse, run, and stringify phases. * * @param file - * `VFile` or anything that can be given to `new VFile()`. - * @param callback - * Callback called with an error or the resulting file. + * File; any value accepted as `x` in `new VFile(x)`. + * @param done + * Callback. * @returns * Nothing. */ process( file: VFileCompatible | undefined, - callback: ProcessCallback> - ): void + done: ProcessCallback> + ): undefined /** - * Process a file. - * - * This performs all phases of the processor: + * Process the given file as configured on the processor. * - * 1. Parse a file into a unist node using the configured `Parser` - * 2. Run transforms on that node - * 3. Compile the resulting node using the `Compiler` + * > 👉 **Note**: `process` freezes the processor if not already *frozen*. * - * The result from the compiler is stored on the file. - * What the result is depends on which plugins you use. - * The result is typically text (`string` or `Uint8Array`), which can be - * retrieved with `file.toString()` (or `String(file)`). - * In some cases, such as when using `rehypeReact` to create a React node, - * the result is stored on `file.result`. + * > 👉 **Note**: `process` performs the parse, run, and stringify phases. * * @param file - * `VFile` or anything that can be given to `new VFile()`. + * File; any value accepted as `x` in `new VFile(x)`. * @returns - * Promise that resolves to the resulting `VFile`. + * `Promise` rejected with a fatal error or resolved with the processed + * file. + * + * The parsed, transformed, and compiled value is available at + * `file.value` (see note). + * + * > 👉 **Note**: unified typically compiles by serializing: most + * > compilers return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different result + * > values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react */ - process(file: VFileCompatible): Promise> + process( + file?: VFileCompatible | undefined + ): Promise> /** - * Process a file, synchronously. - * Throws when asynchronous transforms are configured. + * Process the given file as configured on the processor. * - * This performs all phases of the processor: + * An error is thrown if asynchronous transforms are configured. * - * 1. Parse a file into a unist node using the configured `Parser` - * 2. Run transforms on that node - * 3. Compile the resulting node using the `Compiler` + * > 👉 **Note**: `processSync` freezes the processor if not already *frozen*. * - * The result from the compiler is stored on the file. - * What the result is depends on which plugins you use. - * The result is typically text (`string` or `Uint8Array`), which can be - * retrieved with `file.toString()` (or `String(file)`). - * In some cases, such as when using `rehypeReact` to create a React node, - * the result is stored on `file.result`. + * > 👉 **Note**: `processSync` performs the parse, run, and stringify phases. * * @param file - * `VFile` or anything that can be given to `new VFile()`, optional. + * File; any value accepted as `x` in `new VFile(x)`. * @returns - * Resulting file. + * The processed file. + * + * The parsed, transformed, and compiled value is available at + * `file.value` (see note). + * + * > 👉 **Note**: unified typically compiles by serializing: most + * > compilers return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different result + * > values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react */ processSync( file?: VFileCompatible | undefined ): VFileWithOutput /** - * Get an in-memory key-value store accessible to all phases of the process. + * Configure the processor with info available to all plugins. + * Information is stored in an object. + * + * Typically, options can be given to a specific plugin, but sometimes it + * makes sense to have information shared with several plugins. + * For example, a list of HTML elements that are self-closing, which is + * needed during all phases. * * @returns - * Key-value store. + * The key-value store. */ data(): Record /** - * Set an in-memory key-value store accessible to all phases of the process. + * Configure the processor with info available to all plugins. + * Information is stored in an object. + * + * Typically, options can be given to a specific plugin, but sometimes it + * makes sense to have information shared with several plugins. + * For example, a list of HTML elements that are self-closing, which is + * needed during all phases. + * + * > 👉 **Note**: setting information cannot occur on *frozen* processors. + * > Call the processor first to create a new unfrozen processor. * * @param data - * Key-value store. + * Values to set. * @returns - * Current processor. + * The processor that `data` is called on. */ data( data: Record - ): Processor + ): Processor /** - * Get an in-memory value by key. + * Configure the processor with info available to all plugins. + * Information is stored in an object. + * + * Typically, options can be given to a specific plugin, but sometimes it + * makes sense to have information shared with several plugins. + * For example, a list of HTML elements that are self-closing, which is + * needed during all phases. * * @param key * Key to get. @@ -444,159 +703,185 @@ export type FrozenProcessor< data(key: string): unknown /** - * Set an in-memory value by key. + * Configure the processor with info available to all plugins. + * Information is stored in an object. + * + * Typically, options can be given to a specific plugin, but sometimes it + * makes sense to have information shared with several plugins. + * For example, a list of HTML elements that are self-closing, which is + * needed during all phases. + * + * > 👉 **Note**: setting information cannot occur on *frozen* processors. + * > Call the processor first to create a new unfrozen processor. * * @param key * Key to set. * @param value * Value to set. * @returns - * Current processor. + * The processor that `data` is called on. */ data( key: string, value: unknown - ): Processor + ): Processor /** * Freeze a processor. - * Frozen processors are meant to be extended and not to be configured or - * processed directly. * - * Once a processor is frozen it cannot be unfrozen. - * New processors working just like it can be created by calling the + * Frozen processors are meant to be extended and not to be configured + * directly. + * + * When a processor is frozen it cannot be unfrozen. + * New processors working the same way can be created by calling the * processor. * - * It’s possible to freeze processors explicitly, by calling `.freeze()`, but - * `.parse()`, `.run()`, `.stringify()`, and `.process()` call `.freeze()` to - * freeze a processor too. + * It’s possible to freeze processors explicitly by calling `.freeze()`. + * Processors freeze automatically when `.parse()`, `.run()`, `.runSync()`, + * `.stringify()`, `.process()`, or `.processSync()` are called. + * * * @returns - * Frozen processor. + * The processor that `freeze` was called on. */ - freeze(): FrozenProcessor + freeze(): FrozenProcessor< + ParseTree, + HeadTree, + TailTree, + CompileTree, + CompileResult + > } /** - * A plugin is a function. - * It configures the processor and in turn can receive options. - * Plugins can configure processors by interacting with parsers and compilers - * (at `this.Parser` or `this.Compiler`) or by specifying how the syntax tree - * is handled (by returning a `Transformer`). - * - * @typeParam PluginParameters - * Plugin settings. + * **Plugins** configure the processors they are applied on in the following + * ways: + * + * * they change the processor, such as the parser, the compiler, or by + * configuring data + * * they specify how to handle trees and files + * + * Plugins are a concept. + * They materialize as `Attacher`s. + * + * Attachers are materialized plugins. + * They are functions that can receive options and configure the processor. + * + * Attachers change the processor, such as the parser, the compiler, by + * configuring data, or by specifying how the tree and file are handled. + * + * > 👉 **Note**: attachers are called when the processor is *frozen*, + * > not when they are applied. + * + * @typeParam Parameters + * Arguments passed to the plugin. * @typeParam Input - * Value that is accepted by the plugin. + * Value that is expected as input. * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer expects. - * * If the plugin sets a parser, then this should be `string`. - * * If the plugin sets a compiler, then this should be the node type that - * the compiler expects. + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node it expects. + * * If the plugin sets a {@link Parser `Parser`}, this should be + * `string`. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be the + * node it expects. * @typeParam Output - * Value that the plugin yields. - * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer yields, and defaults to `Input`. - * * If the plugin sets a parser, then this should be the node type that - * the parser yields. - * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Uint8Array`, or something else). + * Value that is yielded as output. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node that that yields. + * * If the plugin sets a {@link Parser `Parser`}, this should be the + * node that it yields. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be + * result it yields. * @this - * The current processor. - * Plugins can configure the processor by interacting with `this.Parser` or - * `this.Compiler`, or by accessing the data associated with the whole process - * (`this.data`). - * @param settings - * Configuration for plugin. + * Processor the attacher is applied to. + * @param parameters + * Arguments passed to the plugin. + * * Plugins typically receive one options object, but could receive other and * more values. - * Users can also pass a boolean instead of settings: `true` (to turn - * a plugin on) or `false` (to turn a plugin off). - * When a plugin is turned off, it won’t be called. - * - * When creating your own plugins, please accept only a single object! - * It allows plugins to be reconfigured and it helps users to know that every - * plugin accepts one options object. * @returns - * Plugins can return a `Transformer` to specify how the syntax tree is - * handled. + * Optional transform. */ export type Plugin< - PluginParameters extends any[] = any[], - Input = Node, + Parameters extends unknown[] = [], + Input extends Node | string | undefined = undefined, Output = Input > = ( - this: Input extends Node - ? Output extends Node - ? // This is a transform, so define `Input` as the current tree. - Processor - : // Compiler. - Processor - : Output extends Node - ? // Parser. - Processor - : // No clue. - Processor, - ...settings: PluginParameters -) => // If both `Input` and `Output` are `Node`, expect an optional `Transformer`. -Input extends Node - ? Output extends Node - ? Transformer | void - : void - : void + this: Processor, + ...parameters: Parameters +) => Input extends string + ? // Parser. + Output extends Node | undefined + ? undefined | void + : never + : Output extends CompileResults + ? // Compiler + Input extends Node | undefined + ? undefined | void + : never + : + | Transformer< + Input extends Node ? Input : Node, + Output extends Node ? Output : Node + > + | undefined + | void /** - * Presets provide a sharable way to configure processors with multiple plugins - * and/or settings. + * Presets are sharable configuration. + * + * They can contain plugins and settings. */ export type Preset = { + /** + * List of plugins and presets. + */ plugins?: PluggableList + + /** + * Shared settings for parsers and compilers. + */ settings?: Record } /** - * A tuple of a plugin and its setting(s). - * The first item is a plugin (function) to use and other items are options. - * Plugins are deduped based on identity: passing a function in twice will - * cause it to run only once. + * Tuple of a plugin and its setting(s). + * The first item is a plugin, the rest are its parameters. * - * @typeParam PluginParameters - * Plugin settings. + * @typeParam Parameters + * Arguments passed to the plugin. * @typeParam Input - * Value that is accepted by the plugin. + * Value that is expected as input. * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer expects. - * * If the plugin sets a parser, then this should be `string`. - * * If the plugin sets a compiler, then this should be the node type that - * the compiler expects. + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node it expects. + * * If the plugin sets a {@link Parser `Parser`}, this should be + * `string`. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be the + * node it expects. * @typeParam Output - * Value that the plugin yields. - * - * * If the plugin returns a transformer, then this should be the node - * type that the transformer yields, and defaults to `Input`. - * * If the plugin sets a parser, then this should be the node type that - * the parser yields. - * * If the plugin sets a compiler, then this should be the result that - * the compiler yields (`string`, `Uint8Array`, or something else). + * Value that is yielded as output. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node that that yields. + * * If the plugin sets a {@link Parser `Parser`}, this should be the + * node that it yields. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be + * result it yields. */ export type PluginTuple< - PluginParameters extends any[] = any[], - Input = Node, - Output = Input -> = [Plugin, ...PluginParameters] + Parameters extends unknown[] = [], + Input extends Node | string | undefined = undefined, + Output = undefined +> = [Plugin, ...Parameters] /** * A union of the different ways to add plugins and settings. - * - * @typeParam PluginParameters - * Plugin settings. */ -export type Pluggable = - | PluginTuple - | Plugin +export type Pluggable = + | Plugin + | PluginTuple | Preset /** @@ -604,94 +889,103 @@ export type Pluggable = */ export type PluggableList = Pluggable[] +// To do: remove? /** + * Attacher. + * * @deprecated * Please use `Plugin`. */ export type Attacher< - PluginParameters extends any[] = any[], - Input = Node, - Output = Input -> = Plugin + Parameters extends unknown[] = unknown[], + Input extends Node | string = Node, + Output extends CompileResults | Node = Input +> = Plugin /** - * Transformers modify the syntax tree or metadata of a file. - * A transformer is a function that is called each time a file is passed - * through the transform phase. - * If an error occurs (either because it’s thrown, returned, rejected, or passed - * to `next`), the process stops. + * Transformers handle syntax trees and files. + * + * They are functions that are called each time a syntax tree and file are + * passed through the run phase. + * When an error occurs in them (either because it’s thrown, returned, + * rejected, or passed to `next`), the process stops. + * + * The run phase is handled by [`trough`][trough], see its documentation for + * the exact semantics of these functions. + * + * [trough]: https://github.com/wooorm/trough#function-fninput-next * * @typeParam Input * Node type that the transformer expects. * @typeParam Output * Node type that the transformer yields. - * @param node - * Tree to be transformed. + * @param tree + * Tree to handle. * @param file - * File associated with node. + * File to handle. * @param next - * Callback that you must call when done. - * Note: this is given if you accept three parameters in your transformer. - * If you accept up to two parameters, it’s not given, and you can return - * a promise. + * Callback. * @returns - * Any of the following: - * - * * `void` — If nothing is returned, the next transformer keeps using same - * tree. - * * `Error` — Can be returned to stop the process. - * * `Node` — Can be returned and results in further transformations and - * `stringify`s to be performed on the new tree. - * * `Promise` — If a promise is returned, the function is asynchronous, and - * must be resolved (optionally with a `Node`) or rejected (optionally with - * an `Error`). - * - * If you accept a `next` callback, nothing should be returned. + * If you accept `next`, nothing. + * Otherwise: + * + * * `Error` — fatal error to stop the process + * * `Promise` or `undefined` — the next transformer keeps using + * same tree + * * `Promise` or `Node` — new, changed, tree */ export type Transformer< Input extends Node = Node, Output extends Node = Input > = ( - node: Input, + tree: Input, file: VFile, next: TransformCallback -) => Promise | Error | Output | undefined | void +) => + | Promise + | Promise // For some reason this is needed separately. + | Output + | Error + | undefined + | void /** - * Callback you must call when a transformer is done. + * If the signature of a `transformer` accepts a third argument, the + * transformer may perform asynchronous operations, and must call `next()`. * * @typeParam Tree - * Node that the plugin yields. + * Node type that the transformer yields. * @param error - * Pass an error to stop the process. - * @param node - * Pass a tree to continue transformations (and `stringify`) on the new tree. + * Fatal error to stop the process (optional). + * @param tree + * New, changed, tree (optional). * @param file - * Pass a file to continue transformations (and `stringify`) on the new file. + * New, changed, file (optional). * @returns * Nothing. */ -export type TransformCallback = ( +export type TransformCallback = ( error?: Error | undefined, - node?: Tree | undefined, + tree?: Output, file?: VFile | undefined -) => void +) => undefined /** - * Function handling the parsing of text to a syntax tree. - * Used in the parse phase in the process and called with a `string` and - * `VFile` representation of the document to parse. + * A **parser** handles the parsing of text to a syntax tree. * - * `Parser` can be a normal function, in which case it must return a `Node`: - * the syntax tree representation of the given file. + * It is used in the parse phase and is called with a `string` and + * {@link VFile `VFile`} of the document to parse. * - * `Parser` can also be a constructor function (a function with keys in its - * `prototype`), in which case it’s called with `new`. - * Instances must have a parse method that is called without arguments and - * must return a `Node`. + * `Parser` can be a normal function, in which case it must return the syntax + * tree representation of the given file ({@link Node `Node`}). + * + * `Parser` can also be a constructor function (a function with a `parse` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `parse` method that is called without arguments and must + * return a {@link Node `Node`}. * * @typeParam Tree - * The node that the parser yields (and `run` receives). + * The node that the parser yields. */ export type Parser = | ParserClass @@ -728,7 +1022,7 @@ export class ParserClass { } /** - * Normal function to parse a file. + * Regular function to parse a file. * * @typeParam Tree * The node that the parser yields. @@ -745,17 +1039,32 @@ export type ParserFunction = ( ) => Tree /** - * Function handling the compilation of syntax tree to a text. - * Used in the stringify phase in the process and called with a `Node` and - * `VFile` representation of the document to stringify. + * A **compiler** handles the compiling of a syntax tree to something else (in + * most cases, text). + * + * It is used in the stringify phase and called with a {@link Node `Node`} + * and {@link VFile `VFile`} representation of the document to compile. + * + * `Compiler` can be a normal function, in which case it should return the + * textual representation of the given tree (`string`). + * + * `Compiler` can also be a constructor function (a function with a `compile` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `compile` method that is called without arguments and + * should return a `string`. * - * `Compiler` can be a normal function, in which case it must return a - * `string`: the text representation of the given syntax tree. + * > 👉 **Note**: unified typically compiles by serializing: most compilers + * > return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different result + * > values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. * - * `Compiler` can also be a constructor function (a function with keys in its - * `prototype`), in which case it’s called with `new`. - * Instances must have a `compile` method that is called without arguments - * and must return a `string`. + * [rehype-react]: https://github.com/rehypejs/rehype-react * * @typeParam Tree * The node that the compiler receives. @@ -800,7 +1109,7 @@ export class CompilerClass { } /** - * Normal function to compile a tree. + * Regular function to compile a tree. * * @typeParam Tree * The node that the compiler receives. @@ -814,47 +1123,52 @@ export class CompilerClass { * New content: compiled text (`string` or `Uint8Array`, for `file.value`) or * something else (for `file.result`). */ -export type CompilerFunction = ( - tree: Tree, - file: VFile -) => Result +export type CompilerFunction< + Tree extends Node = Node, + Result = CompileResults +> = (tree: Tree, file: VFile) => Result /** - * Callback called when a done running. + * Callback called when transformers are done. + * + * Called with either an error or results. + * * @typeParam Tree * The tree that the callback receives. * @param error - * Error passed when unsuccessful. - * @param node - * Tree to transform. + * Fatal error. + * @param tree + * Transformed tree. * @param file - * File passed when successful. + * File. * @returns * Nothing. */ export type RunCallback = ( error?: Error | undefined, - node?: Tree | undefined, + tree?: Tree | undefined, file?: VFile | undefined -) => void +) => undefined /** - * Callback called when a done processing. + * Callback called when the process is done. + * + * Called with either an error or a result. * * @typeParam File * The file that the callback receives. * @param error - * Error passed when unsuccessful. + * Fatal error. * @param file - * File passed when successful. + * Processed file. * @returns * Nothing. */ export type ProcessCallback = ( error?: Error | undefined, file?: File | undefined -) => void +) => undefined /** * A frozen processor. diff --git a/index.test-d.ts b/index.test-d.ts index b51f9fb1..bae224c9 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -39,26 +39,17 @@ const mdastRoot: MdastRoot = { // # Explicitly typed plugins // ## Plugin w/o options -const pluginWithoutOptions: Plugin<[]> = function () { +const pluginWithoutOptions: Plugin = function () { // Empty. } unified().use(pluginWithoutOptions) -unified().use( - pluginWithoutOptions, - // @ts-expect-error: plugin does not expect options. - {} -) -unified().use( - pluginWithoutOptions, - // @ts-expect-error: plugin does not expect `string` as options. - '' -) -unified().use( - pluginWithoutOptions, - // @ts-expect-error: plugin does not expect anything. - undefined -) +// @ts-expect-error: plugin does not expect options. +unified().use(pluginWithoutOptions, {}) +// @ts-expect-error: plugin does not expect `string` as options. +unified().use(pluginWithoutOptions, '') +// @ts-expect-error: plugin does not expect anything. +unified().use(pluginWithoutOptions, undefined) // ## Plugin w/ optional options const pluginWithOptionalOptions: Plugin< @@ -72,11 +63,8 @@ unified().use(pluginWithOptionalOptions, {}) unified().use(pluginWithOptionalOptions, {example: null}) unified().use(pluginWithOptionalOptions, {example: undefined}) unified().use(pluginWithOptionalOptions, {example: 'asd'}) -unified().use( - pluginWithOptionalOptions, - // @ts-expect-error: plugin does not accept `whatever`. - {whatever: 1} -) +// @ts-expect-error: plugin does not accept `whatever`. +unified().use(pluginWithOptionalOptions, {whatever: 1}) // ## Plugin w/ required options const pluginWithOptions: Plugin<[ExampleRequiredOptions]> = function (options) { @@ -85,11 +73,8 @@ const pluginWithOptions: Plugin<[ExampleRequiredOptions]> = function (options) { // @ts-expect-error: plugin requires options. unified().use(pluginWithOptions) -unified().use( - pluginWithOptions, - // @ts-expect-error: plugin requires particular option. - {} -) +// @ts-expect-error: plugin requires particular option. +unified().use(pluginWithOptions, {}) unified().use(pluginWithOptions, {example: ''}) // ## Plugin w/ several arguments @@ -101,16 +86,10 @@ const pluginWithSeveralArguments: Plugin<[ExampleRequiredOptions, number]> = // @ts-expect-error: plugin requires options. unified().use(pluginWithSeveralArguments) -unified().use( - pluginWithSeveralArguments, - // @ts-expect-error: plugin requires particular option. - {} -) -unified().use( - pluginWithSeveralArguments, - // @ts-expect-error: plugin requires more arguments. - {example: ''} -) +// @ts-expect-error: plugin requires particular option. +unified().use(pluginWithSeveralArguments, {}) +// @ts-expect-error: plugin requires more arguments. +unified().use(pluginWithSeveralArguments, {example: ''}) unified().use(pluginWithSeveralArguments, {example: ''}, 1) // # Implicitly typed plugins. @@ -265,7 +244,7 @@ unified() return undefined } }) - // Sync yielding implicit void. + // Sync yielding implicit `void` (because TS). .use(function () { return function () { // Empty. @@ -280,7 +259,7 @@ unified() // Sync throwing error. .use(function () { return function (x) { - // To do: investigate if we can support `never` by dropping this useless condition. + // Note: TS doesn’t like the `never` if we remove this useless condition. if (x) { throw new Error('x') } @@ -338,14 +317,13 @@ unified() return {type: 'x'} } }) - // To do: investigate why TS barfs on `Promise`? - // // Resolving explicit `undefined`. - // .use(function () { - // return async function () { - // return undefined - // } - // }) - // Resolving implicit void. + // Resolving explicit `undefined`. + .use(function () { + return async function () { + return undefined + } + }) + // Resolving implicit `void` (because TS). .use(function () { return async function () { // Empty. @@ -354,7 +332,6 @@ unified() // Rejecting error. .use(function () { return async function (x) { - // To do: investigate if we can support `never` by dropping this useless condition. if (x) { throw new Error('x') } @@ -368,23 +345,13 @@ const remarkParse: Plugin<[], string, MdastRoot> = function () { // Empty. } -const processorWithRemarkParse = unified() - .use(remarkParse) - .use(function () { - return function (tree) { - expectType(tree) - } - }) +const processorWithRemarkParse = unified().use(remarkParse) -expectType>(processorWithRemarkParse) +expectType>(processorWithRemarkParse) expectType(processorWithRemarkParse.parse('')) -// To do: accept `UnistNode`? -expectType(processorWithRemarkParse.runSync(mdastRoot)) -// @ts-expect-error: to do: accept `UnistNode`? -expectType(processorWithRemarkParse.runSync(hastRoot)) -// To do: yield `never`, accept `UnistNode`? -expectType(processorWithRemarkParse.stringify(mdastRoot)) -// @ts-expect-error: to do: accept `UnistNode`? +expectType(processorWithRemarkParse.runSync(mdastRoot)) +expectType(processorWithRemarkParse.runSync(hastRoot)) +expectType(processorWithRemarkParse.stringify(mdastRoot)) processorWithRemarkParse.stringify(hastRoot) expectType(processorWithRemarkParse.processSync('')) @@ -393,25 +360,15 @@ const remarkLint: Plugin<[], MdastRoot> = function () { // Empty. } -const processorWithRemarkLint = unified() - .use(remarkLint) - .use(function () { - return function (tree) { - expectType(tree) - } - }) +const processorWithRemarkLint = unified().use(remarkLint) -// To do: `UnistNode`, `MdastRoot`, `UnistNode`? -expectType>(processorWithRemarkLint) -// To do: yield `UnistNode`? -expectType(processorWithRemarkLint.parse('')) +expectType>(processorWithRemarkLint) +expectType(processorWithRemarkLint.parse('')) expectType(processorWithRemarkLint.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. -expectType(processorWithRemarkLint.runSync(hastRoot)) -// To do: yield `never`, accept `UnistNode`? -expectType(processorWithRemarkLint.stringify(mdastRoot)) -// @ts-expect-error: to do: accept `UnistNode`? -processorWithRemarkLint.stringify(hastRoot) +processorWithRemarkLint.runSync(hastRoot) +expectType(processorWithRemarkLint.stringify(mdastRoot)) +expectType(processorWithRemarkLint.stringify(hastRoot)) expectType(processorWithRemarkLint.processSync('')) // Inspect/transform plugin (implicit). @@ -422,27 +379,21 @@ function remarkLintImplicit() { } } -const processorWithRemarkLintImplicit = unified() - .use(remarkLintImplicit) - .use(function () { - return function (tree) { - expectType(tree) - } - }) +const processorWithRemarkLintImplicit = unified().use(remarkLintImplicit) -// To do: `UnistNode`, `MdastRoot`, `UnistNode`? -expectType>( +expectType>( processorWithRemarkLintImplicit ) -// To do: yield `UnistNode`? -expectType(processorWithRemarkLintImplicit.parse('')) +expectType(processorWithRemarkLintImplicit.parse('')) expectType(processorWithRemarkLintImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkLintImplicit.runSync(hastRoot) -// To do: yield `never`, accept `UnistNode`? -expectType(processorWithRemarkLintImplicit.stringify(mdastRoot)) -// @ts-expect-error: to do: accept `UnistNode`? -processorWithRemarkLintImplicit.stringify(hastRoot) +expectType( + processorWithRemarkLintImplicit.stringify(mdastRoot) +) +expectType( + processorWithRemarkLintImplicit.stringify(hastRoot) +) expectType(processorWithRemarkLintImplicit.processSync('')) // Mutate plugin (explicit). @@ -450,25 +401,15 @@ const remarkRehype: Plugin<[], MdastRoot, HastRoot> = function () { // Empty. } -const processorWithRemarkRehype = unified() - .use(remarkRehype) - .use(function () { - return function (tree) { - expectType(tree) - } - }) +const processorWithRemarkRehype = unified().use(remarkRehype) -// To do: `UnistNode`, `MdastRoot`, `UnistNode`? -expectType>(processorWithRemarkRehype) -// To do: yield `UnistNode`? -expectType(processorWithRemarkRehype.parse('')) +expectType>(processorWithRemarkRehype) +expectType(processorWithRemarkRehype.parse('')) expectType(processorWithRemarkRehype.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehype.runSync(hastRoot) -// To do: yield `never`? -expectType(processorWithRemarkRehype.stringify(hastRoot)) -// @ts-expect-error: to do: accept `UnistNode`? -processorWithRemarkRehype.stringify(mdastRoot) +expectType(processorWithRemarkRehype.stringify(hastRoot)) +expectType(processorWithRemarkRehype.stringify(mdastRoot)) expectType(processorWithRemarkRehype.processSync('')) // Mutate plugin (implicit). @@ -479,27 +420,21 @@ function remarkRehypeImplicit() { } } -const processorWithRemarkRehypeImplicit = unified() - .use(remarkRehypeImplicit) - .use(function () { - return function (tree) { - expectType(tree) - } - }) +const processorWithRemarkRehypeImplicit = unified().use(remarkRehypeImplicit) -// To do: `UnistNode`, `MdastRoot`, `UnistNode`? -expectType>( +expectType>( processorWithRemarkRehypeImplicit ) -// To do: yield `UnistNode`? -expectType(processorWithRemarkRehypeImplicit.parse('')) +expectType(processorWithRemarkRehypeImplicit.parse('')) expectType(processorWithRemarkRehypeImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehypeImplicit.runSync(hastRoot) -// To do: yield `never`? -expectType(processorWithRemarkRehypeImplicit.stringify(hastRoot)) -// @ts-expect-error: to do: accept `UnistNode`? -processorWithRemarkRehypeImplicit.stringify(mdastRoot) +expectType( + processorWithRemarkRehypeImplicit.stringify(hastRoot) +) +expectType( + processorWithRemarkRehypeImplicit.stringify(mdastRoot) +) expectType(processorWithRemarkRehypeImplicit.processSync('')) // Compile plugin. @@ -509,16 +444,12 @@ const rehypeStringify: Plugin<[], HastRoot, string> = function () { const processorWithRehypeStringify = unified().use(rehypeStringify) -// To do: ? -expectType>( +expectType>( processorWithRehypeStringify ) -// To do: yield `UnistNode`? -expectType(processorWithRehypeStringify.parse('')) -// @ts-expect-error: to do: accept `UnistNode`? -processorWithRehypeStringify.runSync(mdastRoot) -// To do: accept, yield `UnistNode`? -expectType(processorWithRehypeStringify.runSync(hastRoot)) +expectType(processorWithRehypeStringify.parse('')) +expectType(processorWithRehypeStringify.runSync(mdastRoot)) +expectType(processorWithRehypeStringify.runSync(hastRoot)) expectType(processorWithRehypeStringify.stringify(hastRoot)) // @ts-expect-error: not the correct node type. processorWithRehypeStringify.stringify(mdastRoot) @@ -534,16 +465,12 @@ const processorWithRehypeStringifyUint8Array = unified().use( rehypeStringifyUint8Array ) -// To do: ? -expectType>( +expectType>( processorWithRehypeStringifyUint8Array ) -// To do: yield `UnistNode`? -expectType(processorWithRehypeStringifyUint8Array.parse('')) -// @ts-expect-error: to do: accept `UnistNode`? -processorWithRehypeStringifyUint8Array.runSync(mdastRoot) -// To do: accept, yield `UnistNode`? -expectType(processorWithRehypeStringifyUint8Array.runSync(hastRoot)) +expectType(processorWithRehypeStringifyUint8Array.parse('')) +expectType(processorWithRehypeStringifyUint8Array.runSync(mdastRoot)) +expectType(processorWithRehypeStringifyUint8Array.runSync(hastRoot)) expectType( processorWithRehypeStringifyUint8Array.stringify(hastRoot) ) @@ -551,6 +478,15 @@ expectType( processorWithRehypeStringifyUint8Array.stringify(mdastRoot) expectType(processorWithRehypeStringifyUint8Array.processSync('')) +/** + * Register our custom compile result. + */ +declare module './index.js' { + interface CompileResultMap { + ReactNode: ReactNode + } +} + // Compile plugin (to a non-node). const rehypeReact: Plugin<[], HastRoot, ReactNode> = function () { // Empty. @@ -558,16 +494,12 @@ const rehypeReact: Plugin<[], HastRoot, ReactNode> = function () { const processorWithRehypeReact = unified().use(rehypeReact) -// To do: ? -expectType>( +expectType>( processorWithRehypeReact ) -// To do: yield `UnistNode`? -expectType(processorWithRehypeReact.parse('')) -// @ts-expect-error: to do: accept `UnistNode`? -processorWithRehypeReact.runSync(mdastRoot) -// To do: accept, yield `UnistNode`? -expectType(processorWithRehypeReact.runSync(hastRoot)) +expectType(processorWithRehypeReact.parse('')) +expectType(processorWithRehypeReact.runSync(mdastRoot)) +expectType(processorWithRehypeReact.runSync(hastRoot)) expectType(processorWithRehypeReact.stringify(hastRoot)) // @ts-expect-error: not the correct node type. processorWithRehypeReact.stringify(mdastRoot) @@ -583,7 +515,9 @@ const processorWithAll = unified() .use(remarkRehype) .use(rehypeStringify) -expectType>(processorWithAll) +expectType>( + processorWithAll +) expectType(processorWithAll.parse('')) expectType(processorWithAll.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. @@ -593,16 +527,23 @@ expectType(processorWithAll.stringify(hastRoot)) processorWithAll.stringify(mdastRoot) expectType(processorWithAll.processSync('')) +// Doesn’t matter how you apply, compiler, transformers, parser is also fine. +expectType>( + unified() + .use(rehypeStringify) + .use(remarkLint) + .use(remarkLintImplicit) + .use(remarkRehype) + .use(remarkParse) +) + // # Different ways to use plugins -expectType>( - unified().use([remarkParse]) -) +expectType(unified().use([remarkParse])) -expectType>( +expectType( unified().use([ remarkParse, - // @ts-expect-error: to do: investigate. remarkLint, remarkLintImplicit, remarkRehype, @@ -610,16 +551,14 @@ expectType>( ]) ) -expectType>( - // @ts-expect-error: to do: investigate. +expectType( unified().use({ plugins: [remarkParse] }) ) -expectType>( +expectType( unified().use({ - // @ts-expect-error: to do: investigate. plugins: [ remarkParse, remarkLint, @@ -630,9 +569,8 @@ expectType>( }) ) -expectType>( +expectType( unified().use({ - // @ts-expect-error: to do: investigate. plugins: [ remarkParse, remarkLint, @@ -666,5 +604,7 @@ const rehypeClassNames: Plugin<[], HastRoot> = function () { // Empty. } -// To do: investigate. -unified().use(remarkLint).use(rehypeClassNames) +// We currently only *use* types, we don’t crash if they are nonsensical. +expectType>( + unified().use(remarkLint).use(rehypeClassNames) +) diff --git a/lib/index.js b/lib/index.js index 670d6fe0..daa54c6c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,12 +8,12 @@ * @typedef {import('../index.js').Parser} Parser * @typedef {import('../index.js').Pluggable} Pluggable * @typedef {import('../index.js').PluggableList} PluggableList + * @typedef {import('../index.js').PluginTuple} PluginTuple * @typedef {import('../index.js').Plugin} Plugin * @typedef {import('../index.js').Preset} Preset * @typedef {import('../index.js').ProcessCallback} ProcessCallback * @typedef {import('../index.js').Processor} Processor * @typedef {import('../index.js').RunCallback} RunCallback - * @typedef {import('../index.js').Transformer} Transformer */ import structuredClone from '@ungap/structured-clone' @@ -52,7 +52,6 @@ function base() { // Plugins. processor.attachers = attachers - // @ts-expect-error: overloads are handled. processor.use = use // API. @@ -75,7 +74,8 @@ function base() { let index = -1 while (++index < attachers.length) { - destination.use(...attachers[index]) + const attacher = attachers[index] + destination.use(...attacher) } destination.data(structuredClone(namespace)) @@ -129,7 +129,6 @@ function base() { options[0] = undefined } - /** @type {Transformer | void} */ const transformer = attacher.call(processor, ...options) if (typeof transformer === 'function') { @@ -144,7 +143,7 @@ function base() { } /** - * @param {Pluggable | null | undefined} [value] + * @param {Exclude | PluggableList | null | undefined} [value] * @param {...unknown} options * @returns {Processor} */ @@ -176,15 +175,16 @@ function base() { return processor /** - * @param {import('../index.js').Pluggable>} value - * @returns {void} + * @param {import('../index.js').Pluggable} value + * @returns {undefined} */ function add(value) { if (typeof value === 'function') { addPlugin(value) } else if (typeof value === 'object') { if (Array.isArray(value)) { - const [plugin, ...options] = value + const [plugin, ...options] = + /** @type {[Plugin, ...Array]} */ (value) addPlugin(plugin, ...options) } else { addPreset(value) @@ -196,7 +196,7 @@ function base() { /** * @param {Preset} result - * @returns {void} + * @returns {undefined} */ function addPreset(result) { if (!('plugins' in result) && !('settings' in result)) { @@ -215,7 +215,7 @@ function base() { /** * @param {PluggableList | null | undefined} [plugins] - * @returns {void} + * @returns {undefined} */ function addList(plugins) { let index = -1 @@ -235,7 +235,7 @@ function base() { /** * @param {Plugin} plugin * @param {...unknown} [value] - * @returns {void} + * @returns {undefined} */ function addPlugin(plugin, value) { let index = -1 @@ -299,7 +299,7 @@ function base() { * @param {Node} node * @param {RunCallback | VFileCompatible} [doc] * @param {RunCallback} [callback] - * @returns {Promise | void} + * @returns {Promise | undefined} */ function run(node, doc, callback) { assertNode(node) @@ -316,10 +316,11 @@ function base() { executor(undefined, callback) + // Note: `void`s needed for TS. /** - * @param {((node: Node) => void) | undefined} resolve - * @param {(error: Error) => void} reject - * @returns {void} + * @param {((node: Node) => undefined | void) | undefined} resolve + * @param {(error: Error) => undefined | void} reject + * @returns {undefined} */ function executor(resolve, reject) { // @ts-expect-error: `doc` can’t be a callback anymore, we checked. @@ -329,7 +330,7 @@ function base() { * @param {Error | undefined} error * @param {Node} tree * @param {VFile} file - * @returns {void} + * @returns {undefined} */ function done(error, tree, file) { tree = tree || node @@ -362,7 +363,7 @@ function base() { /** * @param {Error | undefined} [error] * @param {Node} [tree] - * @returns {void} + * @returns {undefined} */ function done(error, tree) { bail(error) @@ -387,10 +388,11 @@ function base() { executor(undefined, callback) + // Note: `void`s needed for TS. /** - * @param {((file: VFile) => void) | undefined} resolve - * @param {(error?: Error | undefined) => void} reject - * @returns {void} + * @param {((file: VFile) => undefined | void) | undefined} resolve + * @param {(error?: Error | undefined) => undefined | void} reject + * @returns {undefined} */ function executor(resolve, reject) { const file = vfile(doc) @@ -417,7 +419,7 @@ function base() { /** * @param {Error | undefined} [error] * @param {VFile | undefined} [file] - * @returns {void} + * @returns {undefined} */ function done(error, file) { if (error || !file) { @@ -432,7 +434,7 @@ function base() { } } - /** @type {Processor['processSync']} */ + /** @type {import('../index.js').Processor['processSync']} */ function processSync(doc) { /** @type {boolean | undefined} */ let complete @@ -451,7 +453,7 @@ function base() { /** * @param {Error | undefined} [error] - * @returns {void} + * @returns {undefined} */ function done(error) { complete = true diff --git a/package.json b/package.json index b448ae3d..63df3d6e 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "xo": "^0.55.0" }, "scripts": { - "build": "tsc --build --clean && tsc --build && type-coverage", + "build": "tsc --build --clean && tsc --build && type-coverage && tsd", "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", "prepack": "npm run build && npm run format", "test": "npm run build && npm run format && npm run test-coverage", @@ -95,10 +95,6 @@ "atLeast": 100, "detail": true, "ignoreCatch": true, - "#": "`type-coverage` currently barfs on inferring nodes in plugins, while TS gets it", - "ignoreFiles": [ - "test/**/*.js" - ], "strict": true }, "xo": { @@ -109,6 +105,7 @@ ], "rules": { "@typescript-eslint/ban-types": "off", + "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/naming-convention": "off" } } diff --git a/readme.md b/readme.md index 003a4ad4..6ac7a007 100644 --- a/readme.md +++ b/readme.md @@ -460,7 +460,8 @@ Parse text to a syntax tree. ###### Parameters -* `file` ([`VFile`][vfile]) — any value accepted as `x` in `new VFile(x)` +* `file` ([`VFile`][vfile]) — file to parse; typically `string`; any value + accepted as `x` in `new VFile(x)` ###### Returns @@ -497,14 +498,15 @@ Yields: #### `processor.Parser` A **parser** handles the parsing of text to a syntax tree. + It is used in the [parse phase][overview] and is called with a `string` and [`VFile`][vfile] of the document to parse. `Parser` can be a normal function, in which case it must return the syntax tree representation of the given file ([`Node`][node]). -`Parser` can also be a constructor function (a function with a `parse` field, or -other fields, in its `prototype`), in which case it is constructed with `new`. +`Parser` can also be a constructor function (a function with a `parse` field in +its `prototype`), in which case it is constructed with `new`. Instances must have a `parse` method that is called without arguments and must return a [`Node`][node]. @@ -521,8 +523,8 @@ Compile a syntax tree. ###### Parameters * `tree` ([`Node`][node]) — tree to compile -* `file` ([`VFile`][vfile], optional) — any value accepted as `x` in - `new VFile(x)` +* `file` ([`VFile`][vfile], optional) — file associated with `node`; any + value accepted as `x` in `new VFile(x)` ###### Returns @@ -562,6 +564,7 @@ Yields: A **compiler** handles the compiling of a syntax tree to something else (in most cases, text). + It is used in the [stringify phase][overview] and called with a [`Node`][node] and [`VFile`][file] representation of the document to compile. @@ -569,7 +572,7 @@ and [`VFile`][file] representation of the document to compile. representation of the given tree (`string`). `Compiler` can also be a constructor function (a function with a `compile` -field, or other fields, in its `prototype`), in which case it is constructed +field in its `prototype`), in which case it is constructed with `new`. Instances must have a `compile` method that is called without arguments and should return a `string`. @@ -599,7 +602,7 @@ Run *[transformers][transformer]* on a syntax tree. ###### Returns -Nothing if `done` is given (`void`). +Nothing if `done` is given (`undefined`). A [`Promise`][promise] otherwise. The promise is rejected with a fatal error or resolved with the transformed tree ([`Node`][node]). @@ -639,6 +642,7 @@ Yields: #### `function done(err[, tree, file])` Callback called when transformers are done. + Called with either an error or results. ###### Parameters @@ -679,12 +683,13 @@ Process the given file as configured on the processor. ###### Parameters -* `file` ([`VFile`][vfile]) — any value accepted as `x` in `new VFile(x)` +* `file` ([`VFile`][vfile]) — file; any value accepted as `x` in + `new VFile(x)` * `done` ([`Function`][process-done], optional) — callback ###### Returns -Nothing if `done` is given (`void`). +Nothing if `done` is given (`undefined`). A [`Promise`][promise] otherwise. The promise is rejected with a fatal error or resolved with the processed file ([`VFile`][vfile]). @@ -742,6 +747,7 @@ Yields: #### `function done(err, file)` Callback called when the process is done. + Called with either an error or a result. ###### Parameters @@ -905,6 +911,7 @@ processor.data() // => {charlie: 'delta'} ### `processor.freeze()` Freeze a processor. + Frozen processors are meant to be extended and not to be configured directly. When a processor is frozen it cannot be unfrozen. @@ -1073,6 +1080,7 @@ Optional transform ([`Transformer`][transformer]). ### `function transformer(tree, file[, next])` Transformers handle syntax trees and files. + They are functions that are called each time a syntax tree and file are passed through the [run phase][overview]. When an error occurs in them (either because it’s thrown, returned, rejected, @@ -1084,16 +1092,18 @@ exact semantics of these functions. ###### Parameters * `tree` ([`Node`][node]) — tree to handle -* `file` ([`VFile`][vfile]) —file to handle -* `next` ([`Function`][next], optional) +* `file` ([`VFile`][vfile]) — file to handle +* `next` ([`Function`][next], optional) — callback ###### Returns -* `void` — the next transformer keeps using same tree +If you accept `next`, nothing. +Otherwise: + * `Error` — fatal error to stop the process -* [`Node`][node] — new, changed, tree -* `Promise` — resolved with a new, changed, tree or rejected with an - `Error` +* `Promise` or `undefined` — the next transformer keeps using same + tree +* `Promise` or [`Node`][node] — new, changed, tree #### `function next(err[, tree[, file]])` @@ -1109,6 +1119,7 @@ may perform asynchronous operations, and must call `next()`. ## `Preset` Presets are sharable configuration. + They can contain plugins and settings. ###### Example diff --git a/test/freeze.js b/test/freeze.js index fccbbc7c..a4097011 100644 --- a/test/freeze.js +++ b/test/freeze.js @@ -1,15 +1,11 @@ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' -import {SimpleCompiler, SimpleParser} from './util/simple.js' +import {simpleCompiler, simpleParser} from './util/simple.js' test('`freeze`', async function (t) { - const frozen = unified() - .use(function () { - this.Parser = SimpleParser - this.Compiler = SimpleCompiler - }) - .freeze() + const frozen = unified().use(parse).use(compile).freeze() + const unfrozen = frozen() await t.test('data', async function (t) { @@ -189,8 +185,20 @@ test('`freeze`', async function (t) { .use(function () { index++ }) - .use({plugins: [freezingPlugin]}) - .use({plugins: [freezingPlugin]}) + .use({ + plugins: [ + function () { + this.freeze() + } + ] + }) + .use({ + plugins: [ + function () { + this.freeze() + } + ] + }) .freeze() // To show it doesn’t do anything. .freeze() @@ -203,14 +211,24 @@ test('`freeze`', async function (t) { .freeze() assert.equal(index, 2) - - /** - * @satisfies {import('unified').Plugin<[]>} - * @this {import('unified').Processor} - */ - function freezingPlugin() { - this.freeze() - } }) }) }) + +// `this` in JS is buggy in TS. +/** + * @type {import('unified').Plugin<[], string, import('unist').Node>} + */ +function parse() { + // type-coverage:ignore-next-line -- something with TS being wrong. + this.Parser = simpleParser +} + +// `this` in JS is buggy in TS. +/** + * @type {import('unified').Plugin<[], import('unist').Node, string>} + */ +function compile() { + // type-coverage:ignore-next-line -- something with TS being wrong. + this.Compiler = simpleCompiler +} diff --git a/test/process-compilers.js b/test/process-compilers.js index c71a4093..dad6c4c1 100644 --- a/test/process-compilers.js +++ b/test/process-compilers.js @@ -1,14 +1,14 @@ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' -import {SimpleParser} from './util/simple.js' +import {simpleParser} from './util/simple.js' test('process (compilers)', async function (t) { await t.test('should compile `string`', async function () { const processor = unified() const result = 'bravo' - processor.Parser = SimpleParser + processor.Parser = simpleParser processor.Compiler = function () { return result } @@ -23,7 +23,7 @@ test('process (compilers)', async function (t) { const processor = unified() const result = new Uint8Array([0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63]) - processor.Parser = SimpleParser + processor.Parser = simpleParser processor.Compiler = function () { return result } @@ -37,14 +37,13 @@ test('process (compilers)', async function (t) { await t.test('should compile `null`', async function () { const processor = unified() - processor.Parser = SimpleParser + processor.Parser = simpleParser processor.Compiler = function () { return null } const file = await processor.process('alpha') - // To do: is this right? assert.equal(file.value, 'alpha') assert.equal(file.result, undefined) }) @@ -59,7 +58,7 @@ test('process (compilers)', async function (t) { props: {children: ['bravo']} } - processor.Parser = SimpleParser + processor.Parser = simpleParser processor.Compiler = function () { return result } diff --git a/test/process-sync.js b/test/process-sync.js index a315a972..4184298d 100644 --- a/test/process-sync.js +++ b/test/process-sync.js @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' -import {SimpleCompiler, SimpleParser} from './util/simple.js' +import {simpleCompiler, simpleParser} from './util/simple.js' test('`processSync`', async function (t) { await t.test('should throw w/o `Parser`', async function () { @@ -13,7 +13,7 @@ test('`processSync`', async function (t) { await t.test('should throw w/o `Compiler`', async function () { assert.throws(function () { const processor = unified() - processor.Parser = SimpleParser + processor.Parser = simpleParser processor.processSync('') }, /Cannot `processSync` without `Compiler`/) }) @@ -21,8 +21,8 @@ test('`processSync`', async function (t) { await t.test('should support `processSync`', async function () { const processor = unified() - processor.Parser = SimpleParser - processor.Compiler = SimpleCompiler + processor.Parser = simpleParser + processor.Compiler = simpleCompiler assert.equal(processor.processSync('alpha').toString(), 'alpha') }) @@ -31,11 +31,24 @@ test('`processSync`', async function (t) { 'should throw transform errors from `processSync`', async function () { assert.throws(function () { - unified() + const processor = unified() + processor.Parser = simpleParser + processor.Compiler = simpleCompiler + + processor .use(function () { - this.Parser = SimpleParser - this.Compiler = SimpleCompiler + return function () { + return new Error('bravo') + } + }) + .processSync('delta') + }, /Error: bravo/) + assert.throws(function () { + unified() + .use(parse) + .use(compile) + .use(function () { return function () { return new Error('bravo') } @@ -45,3 +58,21 @@ test('`processSync`', async function (t) { } ) }) + +// `this` in JS is buggy in TS. +/** + * @type {import('unified').Plugin<[], string, import('unist').Node>} + */ +function parse() { + // type-coverage:ignore-next-line -- something with TS being wrong. + this.Parser = simpleParser +} + +// `this` in JS is buggy in TS. +/** + * @type {import('unified').Plugin<[], import('unist').Node, string>} + */ +function compile() { + // type-coverage:ignore-next-line -- something with TS being wrong. + this.Compiler = simpleCompiler +} diff --git a/test/process.js b/test/process.js index b43360c3..e2290fb6 100644 --- a/test/process.js +++ b/test/process.js @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import {unified} from 'unified' import {VFile} from 'vfile' -import {SimpleCompiler, SimpleParser} from './util/simple.js' +import {simpleCompiler, simpleParser} from './util/simple.js' test('`process`', async function (t) { const givenFile = new VFile('alpha') @@ -17,7 +17,7 @@ test('`process`', async function (t) { await t.test('should throw w/o `Compiler`', async function () { assert.throws(function () { const processor = unified() - processor.Parser = SimpleParser + processor.Parser = simpleParser processor.process('') }, /Cannot `process` without `Compiler`/) }) @@ -59,8 +59,8 @@ test('`process`', async function (t) { await t.test('should rethrow errors in `done` throws', async function () { const processor = unified() - processor.Parser = SimpleParser - processor.Compiler = SimpleCompiler + processor.Parser = simpleParser + processor.Compiler = simpleCompiler assert.throws(function () { processor.process(givenFile, function () { @@ -74,8 +74,8 @@ test('`process`', async function (t) { async function () { const processor = unified() - processor.Parser = SimpleParser - processor.Compiler = SimpleCompiler + processor.Parser = simpleParser + processor.Compiler = simpleCompiler await new Promise(function (resolve, reject) { processor.process(givenFile).then( diff --git a/test/run-sync.js b/test/run-sync.js index 6af4bb32..8625f2ce 100644 --- a/test/run-sync.js +++ b/test/run-sync.js @@ -152,17 +152,11 @@ test('`runSync`', async function (t) { assert.throws(function () { unified() - .use( - // Note: TS doesn’t understand `Promise`. - /** - * @type {import('unified').Plugin<[]>} - */ - function () { - return async function () { - throw givenError - } + .use(function () { + return async function () { + throw givenError } - ) + }) .runSync(givenNode) }, /`runSync` finished async. Use `run` instead/) }) diff --git a/test/run.js b/test/run.js index 9a8fb1c0..3032fa5b 100644 --- a/test/run.js +++ b/test/run.js @@ -256,18 +256,11 @@ test('`run`', async function (t) { return async function () {} }) // Async transformer w/ explicit `undefined`. - .use( - // Note: TS doesn’t understand w/o explicit `this` type. - /** - * @satisfies {import('unified').Plugin<[]>} - * @this {import('unified').Processor} - */ - function () { - return async function () { - return undefined - } + .use(function () { + return async function () { + return undefined } - ) + }) .use(function () { return async function (tree, file) { assert.equal(tree, givenNode) @@ -342,6 +335,9 @@ test('`run`', async function (t) { function () { reject(new Error('should reject')) }, + /** + * @param {unknown} error + */ function (error) { assert.equal(error, givenError) resolve(undefined) @@ -385,6 +381,9 @@ test('`run`', async function (t) { function () { reject(new Error('should reject')) }, + /** + * @param {unknown} error + */ function (error) { assert.equal(error, givenError) resolve(undefined) @@ -411,6 +410,9 @@ test('`run`', async function (t) { function () { reject(new Error('should reject')) }, + /** + * @param {unknown} error + */ function (error) { assert.equal(error, givenError) resolve(undefined) @@ -477,6 +479,9 @@ test('`run`', async function (t) { function () { reject(new Error('should reject')) }, + /** + * @param {unknown} error + */ function (error) { assert.equal(error, givenError) resolve(undefined) @@ -557,18 +562,11 @@ test('`run`', async function (t) { return async function () {} }) // Async transformer w/ explicit `undefined`. - .use( - // Note: TS doesn’t understand w/o explicit `this` type. - /** - * @satisfies {import('unified').Plugin<[]>} - * @this {import('unified').Processor} - */ - function () { - return async function () { - return undefined - } + .use(function () { + return async function () { + return undefined } - ) + }) .use(function () { return async function (tree, file) { assert.equal(tree, givenNode) diff --git a/test/stringify.js b/test/stringify.js index 652c052a..ad86b19d 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -76,6 +76,7 @@ test('`stringify`', async function (t) { assert.equal(arguments.length, 2) } + // type-coverage:ignore-next-line -- for some reason TS does understand `Parser.prototype`, but not `Compiler.prototype`. processor.Compiler.prototype.compile = function () { assert.equal(arguments.length, 0) return 'echo' diff --git a/test/use.js b/test/use.js index 6ea35de9..4f0f4523 100644 --- a/test/use.js +++ b/test/use.js @@ -11,22 +11,16 @@ test('`use`', async function (t) { await t.test('should ignore no value', function () { const processor = unified() - // To do: investigate if we can enable it. - // @ts-expect-error: check how the runtime handles a missing value. assert.equal(processor.use(), processor) }) await t.test('should ignore `undefined`', function () { const processor = unified() - // To do: investigate if we can enable it. - // @ts-expect-error: check how the runtime handles `undefined`. assert.equal(processor.use(undefined), processor) }) await t.test('should ignore `null`', function () { const processor = unified() - // To do: investigate if we can enable it. - // @ts-expect-error: check how the runtime handles `null`. assert.equal(processor.use(null), processor) }) @@ -99,12 +93,6 @@ test('`use`', async function (t) { assert.equal(arguments.length, 0) calls++ }, - // Note: see if we can remove this line? If we remove the previous `arguments.length` assertion, - // TS infers the plugin fine. But with it, it thinks it’s a tuple? - /** - * @satisfies {import('unified').Plugin<[]>} - * @this {import('unified').Processor} - */ function () { assert.equal(this, processor) assert.equal(arguments.length, 0) @@ -211,10 +199,8 @@ test('`use`', async function (t) { 'should throw when given a preset w/ invalid `plugins` (`false`)', async function () { assert.throws(function () { - unified().use({ - // @ts-expect-error: check how invalid `plugins` is handled. - plugins: false - }) + // @ts-expect-error: check how invalid `plugins` is handled. + unified().use({plugins: false}) }, /Expected a list of plugins, not `false`/) } ) diff --git a/test/util/simple.js b/test/util/simple.js index 41abd306..f0b60f2f 100644 --- a/test/util/simple.js +++ b/test/util/simple.js @@ -1,35 +1,23 @@ /** - * @typedef {import('unified').Parser} Parser - * @typedef {import('unified').Compiler} Compiler + * @typedef {import('unist').Node} Node * @typedef {import('unist').Literal} Literal */ // Make references to the above types visible in VS Code. '' -/** @type {Parser} */ -export class SimpleParser { - /** @param {string} doc */ - constructor(doc) { - /** @type {string} */ - this.value = doc - } - - /** @returns {Literal} */ - parse() { - return {type: 'text', value: this.value} - } +/** + * @param {string} value + * @returns {Literal} + */ +export function simpleParser(value) { + return /** @type {Literal} */ ({type: 'text', value}) } -/** @type {Compiler} */ -export class SimpleCompiler { - /** @param {Literal} node */ - constructor(node) { - /** @type {Literal} */ - this.node = node - } - - compile() { - return this.node.value - } +/** + * @param {Node} node + * @returns {string} + */ +export function simpleCompiler(node) { + return 'value' in node && typeof node.value === 'string' ? node.value : '' } From d5ebd043e96abcdc49a85f992a018546342ab961 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sat, 12 Aug 2023 17:01:26 +0200 Subject: [PATCH 32/46] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63df3d6e..e135f064 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "tsd": "^0.28.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", - "xo": "^0.55.0" + "xo": "^0.56.0" }, "scripts": { "build": "tsc --build --clean && tsc --build && type-coverage && tsd", From cc53bb68ae6679b04e1ac63d67d0989acacbf6e4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Sun, 13 Aug 2023 18:19:16 +0200 Subject: [PATCH 33/46] Refactor to use JSDoc, remove `FrozenProcessor` type --- .gitignore | 1 + index.d.ts | 1181 +------------------------------ index.js | 1 + index.test-d.ts | 37 +- lib/callable-instance.d.ts | 7 + lib/callable-instance.js | 31 + lib/index.js | 1368 ++++++++++++++++++++++++++++-------- package.json | 2 +- script/fix-types.js | 38 + test/core.js | 1 - test/parse.js | 15 +- test/process-compilers.js | 2 + test/stringify.js | 13 +- tsconfig.json | 2 +- 14 files changed, 1236 insertions(+), 1463 deletions(-) create mode 100644 lib/callable-instance.d.ts create mode 100644 lib/callable-instance.js create mode 100644 script/fix-types.js diff --git a/.gitignore b/.gitignore index fcb26070..66103597 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules/ *.log yarn.lock !/index.d.ts +!/lib/callable-instance.d.ts diff --git a/index.d.ts b/index.d.ts index cf53d2af..6a4f5855 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,20 +1,30 @@ -// TypeScript Version: 4.0 - -// Note: this is a `.d.ts` file because it is not possible to have default type -// parameters in JSDoc-based TypeScript, which is a feature we use to type that: -// -// ```js -// .use(somePlugin, theOptions) -// ``` -// -// `theOptions` matches the options that `somePlugin` expects and thus is very -// important for making unified usable in TypeScript. -// -// Furthermore, this is places in the root of the project because types that -// accept type parameters cannot be re-exported as such easily. - -import type {Node} from 'unist' -import type {VFile, VFileCompatible, VFileValue} from 'vfile' +import type {VFileValue} from 'vfile' +import type {CompileResults} from './lib/index.js' + +export type { + // `CompileResultMap` is typed and exposed below. + CompileResults, + Compiler, + CompilerClass, + CompilerFunction, + Pluggable, + PluggableList, + Plugin, + // To do: remove next major. + Plugin as Attacher, + PluginTuple, + Parser, + ParserClass, + ParserFunction, + Preset, + ProcessCallback, + Processor, + RunCallback, + TransformCallback, + Transformer +} from './lib/index.js' + +export {unified} from './lib/index.js' /** * Interface of known results from compilers. @@ -36,1141 +46,10 @@ import type {VFile, VFileCompatible, VFileValue} from 'vfile' * * Use {@link CompileResults `CompileResults`} to access the values. */ -// Note: if `Value` from `VFile` is changed, this should too. export interface CompileResultMap { + // Note: if `Value` from `VFile` is changed, this should too. Uint8Array: Uint8Array string: string + // Empties. + null: null } - -/** - * Acceptable results from compilers. - * - * To register custom results, add them to - * {@link CompileResultMap `CompileResultMap`}. - */ -type CompileResults = CompileResultMap[keyof CompileResultMap] - -/** - * Type to generate a {@link VFile `VFile`} corresponding to a compiler result. - * - * If a result that is not acceptable on a `VFile` is used, that will - * be stored on the `result` field of {@link VFile `VFile`}. - * - * @typeParam Result - * Compile result. - */ -type VFileWithOutput = - Result extends VFileValue | undefined ? VFile : VFile & {result: Result} - -/** - * Create a processor based on the input/output of a {@link Plugin plugin}. - * - * @typeParam ParseTree - * Output of `parse`. - * @typeParam HeadTree - * Input for `run`. - * @typeParam TailTree - * Output for `run`. - * @typeParam CompileTree - * Input of `stringify`. - * @typeParam CompileResult - * Output of `stringify`. - * @typeParam Input - * Input of plugin. - * @typeParam Output - * Output of plugin. - */ -type UsePlugin< - ParseTree extends Node | undefined, - HeadTree extends Node | undefined, - TailTree extends Node | undefined, - CompileTree extends Node | undefined, - CompileResult extends CompileResults | undefined, - Input extends Node | string | undefined, - Output -> = Input extends string - ? Output extends Node | undefined - ? // Parser. - Processor< - Output extends undefined ? ParseTree : Output, - HeadTree, - TailTree, - CompileTree, - CompileResult - > - : // Unknown. - Processor - : Output extends CompileResults - ? Input extends Node | undefined - ? // Compiler. - Processor< - ParseTree, - HeadTree, - TailTree, - Input extends undefined ? CompileTree : Input, - Output extends undefined ? CompileResult : Output - > - : // Unknown. - Processor - : Input extends Node | undefined - ? Output extends Node | undefined - ? // Transform. - Processor< - ParseTree, - // No `HeadTree` yet? Set `Input`. - HeadTree extends undefined ? Input : HeadTree, - Output extends undefined ? TailTree : Output, - CompileTree, - CompileResult - > - : // Unknown. - Processor - : // Unknown. - Processor - -/** - * Processor. - * - * @typeParam ParseTree - * Output of `parse`. - * @typeParam HeadTree - * Input for `run`. - * @typeParam TailTree - * Output for `run`. - * @typeParam CompileTree - * Input of `stringify`. - * @typeParam CompileResult - * Output of `stringify`. - */ -export type Processor< - ParseTree extends Node | undefined = undefined, - HeadTree extends Node | undefined = undefined, - TailTree extends Node | undefined = undefined, - CompileTree extends Node | undefined = undefined, - CompileResult extends CompileResults | undefined = undefined -> = { - /** - * Configure the processor with a preset. - * - * If the processor is already using a plugin, the previous plugin - * configuration is changed based on the options that are passed in. - * In other words, the plugin is not added a second time. - * - * @example - * ```js - * import {unified} from 'unified' - * - * unified() - * // Preset with plugins and settings: - * .use({plugins: [pluginA, [pluginB, {}]], settings: {position: false}}) - * // Settings only: - * .use({settings: {position: false}}) - * ``` - * - * @param preset - * Single preset ({@link Preset `Preset`}): an object with a `plugins` - * and/or `settings`. - * @returns - * Current processor. - */ - use( - preset?: Preset | null | undefined - ): Processor - - /** - * Configure the processor with a list of usable values. - * - * If the processor is already using a plugin, the previous plugin - * configuration is changed based on the options that are passed in. - * In other words, the plugin is not added a second time. - * - * @example - * ```js - * import {unified} from 'unified' - * - * unified() - * // Plugins: - * .use([pluginA, pluginB]) - * // Two plugins, the second with options: - * .use([pluginC, [pluginD, {}]]) - * ``` - * - * @param list - * List of plugins plugins, presets, and tuples - * ({@link PluggableList `PluggableList`}). - * @returns - * Current processor. - */ - use( - list: PluggableList - ): Processor - - /** - * Configure the processor to use a {@link Plugin `Plugin`}. - * - * If the processor is already using a plugin, the previous plugin - * configuration is changed based on the options that are passed in. - * In other words, the plugin is not added a second time. - * - * @example - * ```js - * import {unified} from 'unified' - * - * unified() - * // Plugin with options: - * .use(pluginA, {x: true, y: true}) - * // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): - * .use(pluginA, {y: false, z: true}) - * ``` - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node it expects. - * * If the plugin sets a {@link Parser `Parser`}, this should be - * `string`. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be the - * node it expects. - * @typeParam Output - * Value that is yielded as output. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node that that yields. - * * If the plugin sets a {@link Parser `Parser`}, this should be the - * node that it yields. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be - * result it yields. - * @param plugin - * {@link Plugin `Plugin`} to use. - * @param parameters - * Arguments passed to the {@link Plugin plugin}. - * - * Plugins typically receive one options object, but could receive other and - * more values. - * It’s also possible to pass a boolean: `true` (to turn a plugin on), - * `false` (to turn a plugin off). - * @returns - * Current processor. - */ - use< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = Input - >( - plugin: Plugin, - ...parameters: Parameters | [boolean] - ): UsePlugin< - ParseTree, - HeadTree, - TailTree, - CompileTree, - CompileResult, - Input, - Output - > - - /** - * Configure the processor to use a tuple of a {@link Plugin `Plugin`} with - * its parameters. - * - * If the processor is already using a plugin, the previous plugin - * configuration is changed based on the options that are passed in. - * In other words, the plugin is not added a second time. - * - * @example - * ```js - * import {unified} from 'unified' - * - * unified() - * // Plugin with options: - * .use([pluginA, {x: true, y: true}]) - * ``` - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node it expects. - * * If the plugin sets a {@link Parser `Parser`}, this should be - * `string`. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be the - * node it expects. - * @typeParam Output - * Value that is yielded as output. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node that that yields. - * * If the plugin sets a {@link Parser `Parser`}, this should be the - * node that it yields. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be - * result it yields. - * @param tuple - * {@link Plugin `Plugin`} with arguments to use. - * - * Plugins typically receive one options object, but could receive other and - * more values. - * It’s also possible to pass a boolean: `true` (to turn a plugin on), - * `false` (to turn a plugin off). - * @returns - * Current processor. - */ - use< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = Input - >( - tuple: - | [plugin: Plugin, enable: boolean] // Enable or disable the plugin. - | [plugin: Plugin, ...parameters: Parameters] // Configure the plugin. - ): UsePlugin< - ParseTree, - HeadTree, - TailTree, - CompileTree, - CompileResult, - Input, - Output - > -} & FrozenProcessor - -/** - * Frozen processor. - * - * @typeParam ParseTree - * Output of `parse`. - * @typeParam HeadTree - * Input for `run`. - * @typeParam TailTree - * Output for `run`. - * @typeParam CompileTree - * Input of `stringify`. - * @typeParam CompileResult - * Output of `stringify`. - */ -export type FrozenProcessor< - ParseTree extends Node | undefined = undefined, - HeadTree extends Node | undefined = undefined, - TailTree extends Node | undefined = undefined, - CompileTree extends Node | undefined = undefined, - CompileResult extends CompileResults | undefined = undefined -> = { - /** - * Create a processor. - * - * @returns - * New *unfrozen* processor ({@link Processor `Processor`}) that is - * configured to work the same as its ancestor. - * When the descendant processor is configured in the future it does not - * affect the ancestral processor. - */ - (): Processor - - /** - * Internal list of configured plugins. - * - * @private - */ - attachers: Array> - - /** - * A **parser** handles the parsing of text to a syntax tree. - * - * It is used in the parse phase and is called with a `string` and - * {@link VFile `VFile`} of the document to parse. - * - * `Parser` can be a normal function, in which case it must return the syntax - * tree representation of the given file ({@link Node `Node`}). - * - * `Parser` can also be a constructor function (a function with a `parse` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `parse` method that is called without arguments and must - * return a {@link Node `Node`}. - */ - Parser?: Parser | undefined - - /** - * A **compiler** handles the compiling of a syntax tree to something else (in - * most cases, text). - * - * It is used in the stringify phase and called with a {@link Node `Node`} - * and {@link VFile `VFile`} representation of the document to compile. - * - * `Compiler` can be a normal function, in which case it should return the - * textual representation of the given tree (`string`). - * - * `Compiler` can also be a constructor function (a function with a `compile` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `compile` method that is called without arguments and - * should return a `string`. - * - * > 👉 **Note**: unified typically compiles by serializing: most compilers - * > return `string` (or `Uint8Array`). - * > Some compilers, such as the one configured with - * > [`rehype-react`][rehype-react], return other values (in this case, a - * > React tree). - * > If you’re using a compiler that doesn’t serialize, expect different result - * > values. - * > - * > To register custom results in TypeScript, add them to - * > {@link CompileResultMap `CompileResultMap`}. - * - * [rehype-react]: https://github.com/rehypejs/rehype-react - */ - Compiler?: - | Compiler< - CompileTree extends undefined ? Node : CompileTree, - CompileResult extends undefined ? unknown : CompileResult - > - | undefined - - /** - * Parse text to a syntax tree. - * - * > 👉 **Note**: `parse` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `parse` performs the parse phase, not the run phase or other - * > phases. - * - * @param file - * file to parse; typically `string`; any value accepted as `x` in - * `new VFile(x)`. - * @returns - * Syntax tree representing `file`. - */ - parse( - file?: VFileCompatible | undefined - ): ParseTree extends undefined ? Node : ParseTree - - /** - * Compile a syntax tree. - * - * > 👉 **Note**: `stringify` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `stringify` performs the stringify phase, not the run phase - * or other phases. - * - * @param tree - * Tree to compile - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @returns - * Textual representation of the tree (see note). - * - * > 👉 **Note**: unified typically compiles by serializing: most compilers - * > return `string` (or `Uint8Array`). - * > Some compilers, such as the one configured with - * > [`rehype-react`][rehype-react], return other values (in this case, a - * > React tree). - * > If you’re using a compiler that doesn’t serialize, expect different result - * > values. - * > - * > To register custom results in TypeScript, add them to - * > {@link CompileResultMap `CompileResultMap`}. - * - * [rehype-react]: https://github.com/rehypejs/rehype-react - */ - stringify( - tree: CompileTree extends undefined ? Node : CompileTree, - file?: VFileCompatible | undefined - ): CompileResult extends undefined ? VFileValue : CompileResult - - /** - * Run *transformers* on a syntax tree. - * - * > 👉 **Note**: `run` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `run` performs the run phase, not other phases. - * - * @param tree - * Tree to transform and inspect. - * @param done - * Callback. - * @returns - * Nothing. - */ - run( - tree: HeadTree extends undefined ? Node : HeadTree, - done: RunCallback - ): undefined - - /** - * Run *transformers* on a syntax tree. - * - * > 👉 **Note**: `run` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `run` performs the run phase, not other phases. - * - * @param tree - * Tree to transform and inspect. - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @param done - * Callback. - * @returns - * Nothing. - */ - run( - tree: HeadTree extends undefined ? Node : HeadTree, - file: VFileCompatible | undefined, - done: RunCallback - ): undefined - - /** - * Run *transformers* on a syntax tree. - * - * > 👉 **Note**: `run` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `run` performs the run phase, not other phases. - * - * @param tree - * Tree to transform and inspect. - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @returns - * A `Promise` rejected with a fatal error or resolved with the transformed - * tree. - */ - run( - tree: HeadTree extends undefined ? Node : HeadTree, - file?: VFileCompatible | undefined - ): Promise - - /** - * Run *transformers* on a syntax tree. - * - * An error is thrown if asynchronous transforms are configured. - * - * > 👉 **Note**: `runSync` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `runSync` performs the run phase, not other phases. - * - * @param tree - * Tree to transform and inspect. - * @param file - * File associated with `node` (optional); any value accepted as `x` in - * `new VFile(x)`. - * @returns - * Transformed tree. - */ - runSync( - tree: HeadTree extends undefined ? Node : HeadTree, - file?: VFileCompatible | undefined - ): TailTree extends undefined ? Node : TailTree - - /** - * Process the given file as configured on the processor. - * - * > 👉 **Note**: `process` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `process` performs the parse, run, and stringify phases. - * - * @param file - * File; any value accepted as `x` in `new VFile(x)`. - * @param done - * Callback. - * @returns - * Nothing. - */ - process( - file: VFileCompatible | undefined, - done: ProcessCallback> - ): undefined - - /** - * Process the given file as configured on the processor. - * - * > 👉 **Note**: `process` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `process` performs the parse, run, and stringify phases. - * - * @param file - * File; any value accepted as `x` in `new VFile(x)`. - * @returns - * `Promise` rejected with a fatal error or resolved with the processed - * file. - * - * The parsed, transformed, and compiled value is available at - * `file.value` (see note). - * - * > 👉 **Note**: unified typically compiles by serializing: most - * > compilers return `string` (or `Uint8Array`). - * > Some compilers, such as the one configured with - * > [`rehype-react`][rehype-react], return other values (in this case, a - * > React tree). - * > If you’re using a compiler that doesn’t serialize, expect different result - * > values. - * > - * > To register custom results in TypeScript, add them to - * > {@link CompileResultMap `CompileResultMap`}. - * - * [rehype-react]: https://github.com/rehypejs/rehype-react - */ - process( - file?: VFileCompatible | undefined - ): Promise> - - /** - * Process the given file as configured on the processor. - * - * An error is thrown if asynchronous transforms are configured. - * - * > 👉 **Note**: `processSync` freezes the processor if not already *frozen*. - * - * > 👉 **Note**: `processSync` performs the parse, run, and stringify phases. - * - * @param file - * File; any value accepted as `x` in `new VFile(x)`. - * @returns - * The processed file. - * - * The parsed, transformed, and compiled value is available at - * `file.value` (see note). - * - * > 👉 **Note**: unified typically compiles by serializing: most - * > compilers return `string` (or `Uint8Array`). - * > Some compilers, such as the one configured with - * > [`rehype-react`][rehype-react], return other values (in this case, a - * > React tree). - * > If you’re using a compiler that doesn’t serialize, expect different result - * > values. - * > - * > To register custom results in TypeScript, add them to - * > {@link CompileResultMap `CompileResultMap`}. - * - * [rehype-react]: https://github.com/rehypejs/rehype-react - */ - processSync( - file?: VFileCompatible | undefined - ): VFileWithOutput - - /** - * Configure the processor with info available to all plugins. - * Information is stored in an object. - * - * Typically, options can be given to a specific plugin, but sometimes it - * makes sense to have information shared with several plugins. - * For example, a list of HTML elements that are self-closing, which is - * needed during all phases. - * - * @returns - * The key-value store. - */ - data(): Record - - /** - * Configure the processor with info available to all plugins. - * Information is stored in an object. - * - * Typically, options can be given to a specific plugin, but sometimes it - * makes sense to have information shared with several plugins. - * For example, a list of HTML elements that are self-closing, which is - * needed during all phases. - * - * > 👉 **Note**: setting information cannot occur on *frozen* processors. - * > Call the processor first to create a new unfrozen processor. - * - * @param data - * Values to set. - * @returns - * The processor that `data` is called on. - */ - data( - data: Record - ): Processor - - /** - * Configure the processor with info available to all plugins. - * Information is stored in an object. - * - * Typically, options can be given to a specific plugin, but sometimes it - * makes sense to have information shared with several plugins. - * For example, a list of HTML elements that are self-closing, which is - * needed during all phases. - * - * @param key - * Key to get. - * @returns - * The value at `key`. - */ - data(key: string): unknown - - /** - * Configure the processor with info available to all plugins. - * Information is stored in an object. - * - * Typically, options can be given to a specific plugin, but sometimes it - * makes sense to have information shared with several plugins. - * For example, a list of HTML elements that are self-closing, which is - * needed during all phases. - * - * > 👉 **Note**: setting information cannot occur on *frozen* processors. - * > Call the processor first to create a new unfrozen processor. - * - * @param key - * Key to set. - * @param value - * Value to set. - * @returns - * The processor that `data` is called on. - */ - data( - key: string, - value: unknown - ): Processor - - /** - * Freeze a processor. - * - * Frozen processors are meant to be extended and not to be configured - * directly. - * - * When a processor is frozen it cannot be unfrozen. - * New processors working the same way can be created by calling the - * processor. - * - * It’s possible to freeze processors explicitly by calling `.freeze()`. - * Processors freeze automatically when `.parse()`, `.run()`, `.runSync()`, - * `.stringify()`, `.process()`, or `.processSync()` are called. - * - * - * @returns - * The processor that `freeze` was called on. - */ - freeze(): FrozenProcessor< - ParseTree, - HeadTree, - TailTree, - CompileTree, - CompileResult - > -} - -/** - * **Plugins** configure the processors they are applied on in the following - * ways: - * - * * they change the processor, such as the parser, the compiler, or by - * configuring data - * * they specify how to handle trees and files - * - * Plugins are a concept. - * They materialize as `Attacher`s. - * - * Attachers are materialized plugins. - * They are functions that can receive options and configure the processor. - * - * Attachers change the processor, such as the parser, the compiler, by - * configuring data, or by specifying how the tree and file are handled. - * - * > 👉 **Note**: attachers are called when the processor is *frozen*, - * > not when they are applied. - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node it expects. - * * If the plugin sets a {@link Parser `Parser`}, this should be - * `string`. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be the - * node it expects. - * @typeParam Output - * Value that is yielded as output. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node that that yields. - * * If the plugin sets a {@link Parser `Parser`}, this should be the - * node that it yields. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be - * result it yields. - * @this - * Processor the attacher is applied to. - * @param parameters - * Arguments passed to the plugin. - * - * Plugins typically receive one options object, but could receive other and - * more values. - * @returns - * Optional transform. - */ -export type Plugin< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = Input -> = ( - this: Processor, - ...parameters: Parameters -) => Input extends string - ? // Parser. - Output extends Node | undefined - ? undefined | void - : never - : Output extends CompileResults - ? // Compiler - Input extends Node | undefined - ? undefined | void - : never - : - | Transformer< - Input extends Node ? Input : Node, - Output extends Node ? Output : Node - > - | undefined - | void - -/** - * Presets are sharable configuration. - * - * They can contain plugins and settings. - */ -export type Preset = { - /** - * List of plugins and presets. - */ - plugins?: PluggableList - - /** - * Shared settings for parsers and compilers. - */ - settings?: Record -} - -/** - * Tuple of a plugin and its setting(s). - * The first item is a plugin, the rest are its parameters. - * - * @typeParam Parameters - * Arguments passed to the plugin. - * @typeParam Input - * Value that is expected as input. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node it expects. - * * If the plugin sets a {@link Parser `Parser`}, this should be - * `string`. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be the - * node it expects. - * @typeParam Output - * Value that is yielded as output. - * - * * If the plugin returns a {@link Transformer `Transformer`}, this - * should be the node that that yields. - * * If the plugin sets a {@link Parser `Parser`}, this should be the - * node that it yields. - * * If the plugin sets a {@link Compiler `Compiler`}, this should be - * result it yields. - */ -export type PluginTuple< - Parameters extends unknown[] = [], - Input extends Node | string | undefined = undefined, - Output = undefined -> = [Plugin, ...Parameters] - -/** - * A union of the different ways to add plugins and settings. - */ -export type Pluggable = - | Plugin - | PluginTuple - | Preset - -/** - * A list of plugins and presets. - */ -export type PluggableList = Pluggable[] - -// To do: remove? -/** - * Attacher. - * - * @deprecated - * Please use `Plugin`. - */ -export type Attacher< - Parameters extends unknown[] = unknown[], - Input extends Node | string = Node, - Output extends CompileResults | Node = Input -> = Plugin - -/** - * Transformers handle syntax trees and files. - * - * They are functions that are called each time a syntax tree and file are - * passed through the run phase. - * When an error occurs in them (either because it’s thrown, returned, - * rejected, or passed to `next`), the process stops. - * - * The run phase is handled by [`trough`][trough], see its documentation for - * the exact semantics of these functions. - * - * [trough]: https://github.com/wooorm/trough#function-fninput-next - * - * @typeParam Input - * Node type that the transformer expects. - * @typeParam Output - * Node type that the transformer yields. - * @param tree - * Tree to handle. - * @param file - * File to handle. - * @param next - * Callback. - * @returns - * If you accept `next`, nothing. - * Otherwise: - * - * * `Error` — fatal error to stop the process - * * `Promise` or `undefined` — the next transformer keeps using - * same tree - * * `Promise` or `Node` — new, changed, tree - */ -export type Transformer< - Input extends Node = Node, - Output extends Node = Input -> = ( - tree: Input, - file: VFile, - next: TransformCallback -) => - | Promise - | Promise // For some reason this is needed separately. - | Output - | Error - | undefined - | void - -/** - * If the signature of a `transformer` accepts a third argument, the - * transformer may perform asynchronous operations, and must call `next()`. - * - * @typeParam Tree - * Node type that the transformer yields. - * @param error - * Fatal error to stop the process (optional). - * @param tree - * New, changed, tree (optional). - * @param file - * New, changed, file (optional). - * @returns - * Nothing. - */ -export type TransformCallback = ( - error?: Error | undefined, - tree?: Output, - file?: VFile | undefined -) => undefined - -/** - * A **parser** handles the parsing of text to a syntax tree. - * - * It is used in the parse phase and is called with a `string` and - * {@link VFile `VFile`} of the document to parse. - * - * `Parser` can be a normal function, in which case it must return the syntax - * tree representation of the given file ({@link Node `Node`}). - * - * `Parser` can also be a constructor function (a function with a `parse` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `parse` method that is called without arguments and must - * return a {@link Node `Node`}. - * - * @typeParam Tree - * The node that the parser yields. - */ -export type Parser = - | ParserClass - | ParserFunction - -/** - * A class to parse files. - * - * @typeParam Tree - * The node that the parser yields. - */ -export class ParserClass { - prototype: { - /** - * Parse a file. - * - * @returns - * Parsed tree. - */ - parse(): Tree - } - - /** - * Constructor. - * - * @param document - * Document to parse. - * @param file - * File associated with `document`. - * @returns - * Instance. - */ - constructor(document: string, file: VFile) -} - -/** - * Regular function to parse a file. - * - * @typeParam Tree - * The node that the parser yields. - * @param document - * Document to parse. - * @param file - * File associated with `document`. - * @returns - * Node representing the given file. - */ -export type ParserFunction = ( - document: string, - file: VFile -) => Tree - -/** - * A **compiler** handles the compiling of a syntax tree to something else (in - * most cases, text). - * - * It is used in the stringify phase and called with a {@link Node `Node`} - * and {@link VFile `VFile`} representation of the document to compile. - * - * `Compiler` can be a normal function, in which case it should return the - * textual representation of the given tree (`string`). - * - * `Compiler` can also be a constructor function (a function with a `compile` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `compile` method that is called without arguments and - * should return a `string`. - * - * > 👉 **Note**: unified typically compiles by serializing: most compilers - * > return `string` (or `Uint8Array`). - * > Some compilers, such as the one configured with - * > [`rehype-react`][rehype-react], return other values (in this case, a - * > React tree). - * > If you’re using a compiler that doesn’t serialize, expect different result - * > values. - * > - * > To register custom results in TypeScript, add them to - * > {@link CompileResultMap `CompileResultMap`}. - * - * [rehype-react]: https://github.com/rehypejs/rehype-react - * - * @typeParam Tree - * The node that the compiler receives. - * @typeParam Result - * The thing that the compiler yields. - */ -export type Compiler = - | CompilerClass - | CompilerFunction - -/** - * A class to compile trees. - * - * @typeParam Tree - * The node that the compiler receives. - * @typeParam Result - * The thing that the compiler yields. - */ -export class CompilerClass { - prototype: { - /** - * Compile a tree. - * - * @returns - * New content: compiled text (`string` or `Uint8Array`, for - * `file.value`) or something else (for `file.result`). - */ - compile(): Result - } - - /** - * Constructor. - * - * @param tree - * Tree to compile. - * @param file - * File associated with `tree`. - * @returns - * Instance. - */ - constructor(tree: Tree, file: VFile) -} - -/** - * Regular function to compile a tree. - * - * @typeParam Tree - * The node that the compiler receives. - * @typeParam Result - * The thing that the compiler yields. - * @param tree - * Tree to compile. - * @param file - * File associated with `tree`. - * @returns - * New content: compiled text (`string` or `Uint8Array`, for `file.value`) or - * something else (for `file.result`). - */ -export type CompilerFunction< - Tree extends Node = Node, - Result = CompileResults -> = (tree: Tree, file: VFile) => Result - -/** - * Callback called when transformers are done. - * - * Called with either an error or results. - - * - * @typeParam Tree - * The tree that the callback receives. - * @param error - * Fatal error. - * @param tree - * Transformed tree. - * @param file - * File. - * @returns - * Nothing. - */ -export type RunCallback = ( - error?: Error | undefined, - tree?: Tree | undefined, - file?: VFile | undefined -) => undefined - -/** - * Callback called when the process is done. - * - * Called with either an error or a result. - * - * @typeParam File - * The file that the callback receives. - * @param error - * Fatal error. - * @param file - * Processed file. - * @returns - * Nothing. - */ -export type ProcessCallback = ( - error?: Error | undefined, - file?: File | undefined -) => undefined - -/** - * A frozen processor. - */ -export function unified(): Processor diff --git a/index.js b/index.js index 024230ea..43197543 100644 --- a/index.js +++ b/index.js @@ -1 +1,2 @@ +// Note: types exposed from `index.d.ts`. export {unified} from './lib/index.js' diff --git a/index.test-d.ts b/index.test-d.ts index bae224c9..2523938e 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -3,16 +3,11 @@ import type {Root as MdastRoot} from 'mdast' import {expectType} from 'tsd' import type {Node as UnistNode} from 'unist' import type {VFile} from 'vfile' -import type { - FrozenProcessor, - Plugin, - Processor, - TransformCallback -} from './index.js' +import type {Plugin, Processor, TransformCallback} from './index.js' import {unified} from './index.js' expectType(unified()) -expectType(unified().freeze()) +expectType(unified().freeze()) type ReactNode = { kind: string @@ -351,7 +346,9 @@ expectType>(processorWithRemarkParse) expectType(processorWithRemarkParse.parse('')) expectType(processorWithRemarkParse.runSync(mdastRoot)) expectType(processorWithRemarkParse.runSync(hastRoot)) -expectType(processorWithRemarkParse.stringify(mdastRoot)) +expectType( + processorWithRemarkParse.stringify(mdastRoot) +) processorWithRemarkParse.stringify(hastRoot) expectType(processorWithRemarkParse.processSync('')) @@ -367,8 +364,12 @@ expectType(processorWithRemarkLint.parse('')) expectType(processorWithRemarkLint.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkLint.runSync(hastRoot) -expectType(processorWithRemarkLint.stringify(mdastRoot)) -expectType(processorWithRemarkLint.stringify(hastRoot)) +expectType( + processorWithRemarkLint.stringify(mdastRoot) +) +expectType( + processorWithRemarkLint.stringify(hastRoot) +) expectType(processorWithRemarkLint.processSync('')) // Inspect/transform plugin (implicit). @@ -388,10 +389,10 @@ expectType(processorWithRemarkLintImplicit.parse('')) expectType(processorWithRemarkLintImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkLintImplicit.runSync(hastRoot) -expectType( +expectType( processorWithRemarkLintImplicit.stringify(mdastRoot) ) -expectType( +expectType( processorWithRemarkLintImplicit.stringify(hastRoot) ) expectType(processorWithRemarkLintImplicit.processSync('')) @@ -408,8 +409,12 @@ expectType(processorWithRemarkRehype.parse('')) expectType(processorWithRemarkRehype.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehype.runSync(hastRoot) -expectType(processorWithRemarkRehype.stringify(hastRoot)) -expectType(processorWithRemarkRehype.stringify(mdastRoot)) +expectType( + processorWithRemarkRehype.stringify(hastRoot) +) +expectType( + processorWithRemarkRehype.stringify(mdastRoot) +) expectType(processorWithRemarkRehype.processSync('')) // Mutate plugin (implicit). @@ -429,10 +434,10 @@ expectType(processorWithRemarkRehypeImplicit.parse('')) expectType(processorWithRemarkRehypeImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehypeImplicit.runSync(hastRoot) -expectType( +expectType( processorWithRemarkRehypeImplicit.stringify(hastRoot) ) -expectType( +expectType( processorWithRemarkRehypeImplicit.stringify(mdastRoot) ) expectType(processorWithRemarkRehypeImplicit.processSync('')) diff --git a/lib/callable-instance.d.ts b/lib/callable-instance.d.ts new file mode 100644 index 00000000..0fbc88f7 --- /dev/null +++ b/lib/callable-instance.d.ts @@ -0,0 +1,7 @@ +type Func = (...argv: Args) => Return + +export type ICallableInstance = new ( + property: string | symbol +) => Func + +export const CallableInstance: ICallableInstance diff --git a/lib/callable-instance.js b/lib/callable-instance.js new file mode 100644 index 00000000..6c092d88 --- /dev/null +++ b/lib/callable-instance.js @@ -0,0 +1,31 @@ +/* eslint-disable unicorn/no-this-assignment */ +/** + * @param {string} property + */ +export function CallableInstance(property) { + /** @type {Function} */ + const self = this + const constr = self.constructor + // Prototypes do exist. + // type-coverage:ignore-next-line + const proto = /** @type {Record} */ (constr.prototype) + const func = proto[property] + const apply = function () { + return func.apply(apply, arguments) + } + + Object.setPrototypeOf(apply, proto) + + const names = Object.getOwnPropertyNames(func) + + for (const p of names) { + const descriptor = Object.getOwnPropertyDescriptor(func, p) + if (descriptor) Object.defineProperty(apply, p, descriptor) + } + + return apply +} + +// Prototypes do exist. +// type-coverage:ignore-next-line +CallableInstance.prototype = Object.create(Function.prototype) diff --git a/lib/index.js b/lib/index.js index daa54c6c..f81bc3bd 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,19 +1,392 @@ +/* eslint-disable unicorn/no-this-assignment */ /** + * @typedef {import('trough').Pipeline} Pipeline + * * @typedef {import('unist').Node} Node * * @typedef {import('vfile').VFileCompatible} VFileCompatible * @typedef {import('vfile').VFileValue} VFileValue * - * @typedef {import('../index.js').Compiler} Compiler - * @typedef {import('../index.js').Parser} Parser - * @typedef {import('../index.js').Pluggable} Pluggable - * @typedef {import('../index.js').PluggableList} PluggableList - * @typedef {import('../index.js').PluginTuple} PluginTuple - * @typedef {import('../index.js').Plugin} Plugin - * @typedef {import('../index.js').Preset} Preset - * @typedef {import('../index.js').ProcessCallback} ProcessCallback - * @typedef {import('../index.js').Processor} Processor - * @typedef {import('../index.js').RunCallback} RunCallback + * @typedef {import('../index.js').CompileResultMap} CompileResultMap + */ + +/** + * @typedef {CompileResultMap[keyof CompileResultMap]} CompileResults + * Acceptable results from compilers. + * + * To register custom results, add them to + * {@link CompileResultMap `CompileResultMap`}. + */ + +/** + * @template {Node} [Tree=Node] + * The node that the compiler receives. + * @template {CompileResults} [Result=CompileResults] + * The thing that the compiler yields. + * @typedef {CompilerClass | CompilerFunction} Compiler + * A **compiler** handles the compiling of a syntax tree to something else + * (in most cases, text). + * + * It is used in the stringify phase and called with a {@link Node `Node`} + * and {@link VFile `VFile`} representation of the document to compile. + * + * `Compiler` can be a normal function, in which case it should return the + * textual representation of the given tree (`string`). + * + * `Compiler` can also be a constructor function (a function with a `compile` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `compile` method that is called without arguments + * and should return a `string`. + * + * > 👉 **Note**: unified typically compiles by serializing: most compilers + * > return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different + * > result values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react + */ + +/** + * @template {Node} [Tree=Node] + * @template {CompileResults} [Result=CompileResults] + * @typedef {({ + * prototype: {compile(): Result} + * new (tree: Tree, file: VFile): CompilerClass['prototype'] + * })} CompilerClass + * Class to compile trees. + */ + +/** + * @template {Node} [Tree=Node] + * The node that the compiler receives. + * @template {CompileResults} [Result=CompileResults] + * The thing that the compiler yields. + * @callback CompilerFunction + * Regular function to compile a tree. + * @param {Tree} tree + * Tree to compile. + * @param {VFile} file + * File associated with `tree`. + * @returns {Result} + * New content: compiled text (`string` or `Uint8Array`, for `file.value`) or + * something else (for `file.result`). + */ + +/** + * @template {Node} [Tree=Node] + * The node that the parser yields. + * @typedef {ParserClass | ParserFunction} Parser + * A **parser** handles the parsing of text to a syntax tree. + * + * It is used in the parse phase and is called with a `string` and + * {@link VFile `VFile`} of the document to parse. + * + * `Parser` can be a normal function, in which case it must return the syntax + * tree representation of the given file ({@link Node `Node`}). + * + * `Parser` can also be a constructor function (a function with a `parse` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `parse` method that is called without arguments and + * must return a {@link Node `Node`}. + */ + +/** + * @template {Node} [Tree=Node] + * @typedef {({ + * prototype: {parse(): Tree} + * new (document: string, file: VFile): ParserClass['prototype'] + * })} ParserClass + * Class to parse files. + */ + +/** + * @template {Node} [Tree=Node] + * The node that the parser yields. + * @callback ParserFunction + * Regular function to parse a file. + * @param {string} document + * Document to parse. + * @param {VFile} file + * File associated with `document`. + * @returns {Tree} + * Node representing the given file. + */ + +/** + * @typedef {( + * Plugin, any, any> | + * PluginTuple, any, any> | + * Preset + * )} Pluggable + * Union of the different ways to add plugins and settings. + */ + +/** + * @typedef {Array} PluggableList + * List of plugins and presets. + */ + +// Note: we can’t use `callback` yet as it messes up `this`: +// . +/** + * @template {Array} [PluginParameters=[]] + * Arguments passed to the plugin. + * @template {Node | string | undefined} [Input=Node] + * Value that is expected as input. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node it expects. + * * If the plugin sets a {@link Parser `Parser`}, this should be + * `string`. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be the + * node it expects. + * @template [Output=Input] + * Value that is yielded as output. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node that that yields. + * * If the plugin sets a {@link Parser `Parser`}, this should be the + * node that it yields. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be + * result it yields. + * @typedef {( + * (this: Processor, ...parameters: PluginParameters) => + * Input extends string ? // Parser. + * Output extends Node | undefined ? undefined | void : never : + * Output extends CompileResults ? // Compiler. + * Input extends Node | undefined ? undefined | void : never : + * Transformer< + * Input extends Node ? Input : Node, + * Output extends Node ? Output : Node + * > | undefined | void + * )} Plugin + * **Plugins** configure the processors they are applied on in the following + * ways: + * + * * they change the processor, such as the parser, the compiler, or by + * configuring data + * * they specify how to handle trees and files + * + * Plugins are a concept. + * They materialize as `Attacher`s. + * + * Attachers are materialized plugins. + * They are functions that can receive options and configure the processor. + * + * Attachers change the processor, such as the parser, the compiler, by + * configuring data, or by specifying how the tree and file are handled. + * + * > 👉 **Note**: attachers are called when the processor is *frozen*, + * > not when they are applied. + */ + +/** + * Tuple of a plugin and its setting(s). + * The first item is a plugin, the rest are its parameters. + * + * @template {Array} [TupleParameters=[]] + * Arguments passed to the plugin. + * @template {Node | string | undefined} [Input=undefined] + * Value that is expected as input. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node it expects. + * * If the plugin sets a {@link Parser `Parser`}, this should be + * `string`. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be the + * node it expects. + * @template [Output=undefined] + * @typedef {( + * [ + * plugin: Plugin, + * ...parameters: TupleParameters + * ] + * )} PluginTuple + * Value that is yielded as output. + * + * * If the plugin returns a {@link Transformer `Transformer`}, this + * should be the node that that yields. + * * If the plugin sets a {@link Parser `Parser`}, this should be the + * node that it yields. + * * If the plugin sets a {@link Compiler `Compiler`}, this should be + * result it yields. + */ + +/** + * @typedef Preset + * Presets are sharable configuration. + * + * They can contain plugins and settings. + * @property {PluggableList | undefined} [plugins] + * List of plugins and presets. + * @property {Record | undefined} [settings] + * Shared settings for parsers and compilers. + */ + +/** + * @template {VFile} [File=VFile] + * The file that the callback receives. + * @callback ProcessCallback + * Callback called when the process is done. + * + * Called with either an error or a result. + * @param {Error | undefined} [error] + * Fatal error. + * @param {File | undefined} [file] + * Processed file. + * @returns {undefined} + * Nothing. + */ + +/** + * @template {Node} [Tree=Node] + * The tree that the callback receives. + * @callback RunCallback + * Callback called when transformers are done. + * + * Called with either an error or results. + * @param {Error | undefined} [error] + * Fatal error. + * @param {Tree | undefined} [tree] + * Transformed tree. + * @param {VFile | undefined} [file] + * File. + * @returns {undefined} + * Nothing. + */ + +/** + * @template {Node} [Input=Node] + * Node type that the transformer expects. + * @template {Node} [Output=Input] + * Node type that the transformer yields. + * @callback Transformer + * Transformers handle syntax trees and files. + * + * They are functions that are called each time a syntax tree and file are + * passed through the run phase. + * When an error occurs in them (either because it’s thrown, returned, + * rejected, or passed to `next`), the process stops. + * + * The run phase is handled by [`trough`][trough], see its documentation for + * the exact semantics of these functions. + * + * [trough]: https://github.com/wooorm/trough#function-fninput-next + * @param {Input} tree + * Tree to handle. + * @param {VFile} file + * File to handle. + * @param {TransformCallback} next + * Callback. + * @returns {( + * Promise | + * Promise | // For some reason this is needed separately. + * Output | + * Error | + * undefined | + * void + * )} + * If you accept `next`, nothing. + * Otherwise: + * + * * `Error` — fatal error to stop the process + * * `Promise` or `undefined` — the next transformer keeps using + * same tree + * * `Promise` or `Node` — new, changed, tree + */ + +/** + * @template {Node} [Output=Node] + * Node type that the transformer yields. + * @callback TransformCallback + * If the signature of a `transformer` accepts a third argument, the + * transformer may perform asynchronous operations, and must call `next()`. + * @param {Error | undefined} [error] + * Fatal error to stop the process (optional). + * @param {Output | undefined} [tree] + * New, changed, tree (optional). + * @param {VFile | undefined} [file] + * New, changed, file (optional). + * @returns {undefined} + * Nothing. + */ + +/** + * @template {Node | undefined} ParseTree + * Output of `parse`. + * @template {Node | undefined} HeadTree + * Input for `run`. + * @template {Node | undefined} TailTree + * Output for `run`. + * @template {Node | undefined} CompileTree + * Input of `stringify`. + * @template {CompileResults | undefined} CompileResult + * Output of `stringify`. + * @template {Node | string | undefined} Input + * Input of plugin. + * @template Output + * Output of plugin. + * @typedef {( + * Input extends string + * ? Output extends Node | undefined + * ? // Parser. + * Processor< + * Output extends undefined ? ParseTree : Output, + * HeadTree, + * TailTree, + * CompileTree, + * CompileResult + * > + * : // Unknown. + * Processor + * : Output extends CompileResults + * ? Input extends Node | undefined + * ? // Compiler. + * Processor< + * ParseTree, + * HeadTree, + * TailTree, + * Input extends undefined ? CompileTree : Input, + * Output extends undefined ? CompileResult : Output + * > + * : // Unknown. + * Processor + * : Input extends Node | undefined + * ? Output extends Node | undefined + * ? // Transform. + * Processor< + * ParseTree, + * HeadTree extends undefined ? Input : HeadTree, + * Output extends undefined ? TailTree : Output, + * CompileTree, + * CompileResult + * > + * : // Unknown. + * Processor + * : // Unknown. + * Processor + * )} UsePlugin + * Create a processor based on the input/output of a {@link Plugin plugin}. + */ + +/** + * @template {CompileResults | undefined} Result + * Node type that the transformer yields. + * @typedef {( + * Result extends VFileValue | undefined ? + * VFile : + * VFile & {result: Result} + * )} VFileWithOutput + * Type to generate a {@link VFile `VFile`} corresponding to a compiler result. + * + * If a result that is not acceptable on a `VFile` is used, that will + * be stored on the `result` field of {@link VFile `VFile`}. */ import structuredClone from '@ungap/structured-clone' @@ -21,105 +394,260 @@ import {bail} from 'bail' import isPlainObj from 'is-plain-obj' import {trough} from 'trough' import {VFile} from 'vfile' - -// Expose a frozen processor. -export const unified = base().freeze() +import {CallableInstance} from './callable-instance.js' const own = {}.hasOwnProperty -// Function to create the first processor. -/** - * @returns {Processor} - */ -function base() { - const transformers = trough() - /** @type {Processor['attachers']} */ - const attachers = [] - /** @type {{settings?: Record} & Record} */ - let namespace = {} - /** @type {boolean | undefined} */ - let frozen - let freezeIndex = -1 - - // Data management. - // @ts-expect-error: overloads are handled. - processor.data = data - processor.Parser = undefined - processor.Compiler = undefined - - // Lock. - processor.freeze = freeze - - // Plugins. - processor.attachers = attachers - processor.use = use - - // API. - processor.parse = parse - processor.stringify = stringify - // @ts-expect-error: overloads are handled. - processor.run = run - processor.runSync = runSync - // @ts-expect-error: overloads are handled. - processor.process = process - processor.processSync = processSync - - // Expose. - return processor - - // Create a new processor based on the processor in the current scope. - /** @type {Processor} */ - function processor() { - const destination = base() +/** + * @template {Node | undefined} [ParseTree=undefined] + * Output of `parse`. + * @template {Node | undefined} [HeadTree=undefined] + * Input for `run`. + * @template {Node | undefined} [TailTree=undefined] + * Output for `run`. + * @template {Node | undefined} [CompileTree=undefined] + * Input of `stringify`. + * @template {CompileResults | undefined} [CompileResult=undefined] + * Output of `stringify`. + * @extends {CallableInstance<[], Processor>} + */ +export class Processor extends CallableInstance { + /** + * Create a processor. + */ + constructor() { + // If `Processor()` is called (w/o new), `copy` is called instead. + super('copy') + + /** + * A **compiler** handles the compiling of a syntax tree to something else + * (in most cases, text). + * + * It is used in the stringify phase and called with a {@link Node `Node`} + * and {@link VFile `VFile`} representation of the document to compile. + * + * `Compiler` can be a normal function, in which case it should return the + * textual representation of the given tree (`string`). + * + * `Compiler` can also be a constructor function (a function with a + * `compile` field in its `prototype`), in which case it is constructed with + * `new`. + * Instances must have a `compile` method that is called without arguments + * and should return a `string`. + * + * > 👉 **Note**: unified typically compiles by serializing: most compilers + * > return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different + * > result values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react + * + * @type {( + * Compiler< + * CompileTree extends undefined ? Node : CompileTree, + * CompileResult extends undefined ? CompileResults : CompileResult + * > | + * undefined + * )} + */ + this.Compiler = undefined + + /** + * A **parser** handles the parsing of text to a syntax tree. + * + * It is used in the parse phase and is called with a `string` and + * {@link VFile `VFile`} of the document to parse. + * + * `Parser` can be a normal function, in which case it must return the + * syntax tree representation of the given file ({@link Node `Node`}). + * + * `Parser` can also be a constructor function (a function with a `parse` + * field in its `prototype`), in which case it is constructed with `new`. + * Instances must have a `parse` method that is called without arguments + * and must return a {@link Node `Node`}. + * + * @type {( + * Parser | + * undefined + * )} + */ + this.Parser = undefined + + // Note: the following fields are considered private. + // However, they are needed for tests, and TSC generates an untyped + // `private freezeIndex` field for, which trips `type-coverage` up. + // Instead, we use `@deprecated` to visualize that they shouldn’t be used. + /** + * Internal list of configured plugins. + * + * @deprecated + * This is a private internal property and should not be used. + * @type {Array>>} + */ + this.attachers = [] + + /** + * Internal state to track where we are while freezing. + * + * @deprecated + * This is a private internal property and should not be used. + * @type {number} + */ + this.freezeIndex = -1 + + /** + * Internal state to track whether we’re frozen. + * + * @deprecated + * This is a private internal property and should not be used. + * @type {boolean | undefined} + */ + this.frozen = undefined + + /** + * Internal state. + * + * @deprecated + * This is a private internal property and should not be used. + * @type {{settings?: Record} & Record} + */ + this.namespace = {} + + /** + * Internal list of configured transformers. + * + * @deprecated + * This is a private internal property and should not be used. + * @type {Pipeline} + */ + this.transformers = trough() + } + + /** + * Copy a processor. + * + * @deprecated + * This is a private internal method and should not be used. + * @returns {Processor} + * New *unfrozen* processor ({@link Processor `Processor`}) that is + * configured to work the same as its ancestor. + * When the descendant processor is configured in the future it does not + * affect the ancestral processor. + */ + copy() { + // Cast as the type parameters will be the same after attaching. + const destination = + /** @type {Processor} */ ( + new Processor() + ) let index = -1 - while (++index < attachers.length) { - const attacher = attachers[index] + while (++index < this.attachers.length) { + const attacher = this.attachers[index] destination.use(...attacher) } - destination.data(structuredClone(namespace)) + destination.data(structuredClone(this.namespace)) return destination } /** + * Configure the processor with info available to all plugins. + * Information is stored in an object. + * + * Typically, options can be given to a specific plugin, but sometimes it + * makes sense to have information shared with several plugins. + * For example, a list of HTML elements that are self-closing, which is + * needed during all phases. + * + * > 👉 **Note**: setting information cannot occur on *frozen* processors. + * > Call the processor first to create a new unfrozen processor. + * + * @overload + * @returns {Record} + * + * @overload + * @param {Record} dataset + * @returns {Processor} + * + * @overload + * @param {string} key + * @returns {unknown} + * + * @overload + * @param {string} key + * @param {unknown} value + * @returns {Processor} + * * @param {Record | string} [key] + * Key to get or set, or entire dataset to set, or nothing to get the + * entire dataset. * @param {unknown} [value] + * Value to set. * @returns {unknown} + * The processor that `data` is called on when settings, the value at `key` + * when getting, or the entire dataset when getting w/o key. */ - function data(key, value) { + data(key, value) { if (typeof key === 'string') { // Set `key`. if (arguments.length === 2) { - assertUnfrozen('data', frozen) - namespace[key] = value - return processor + assertUnfrozen('data', this.frozen) + this.namespace[key] = value + return this } // Get `key`. - return (own.call(namespace, key) && namespace[key]) || undefined + return (own.call(this.namespace, key) && this.namespace[key]) || undefined } // Set space. if (key) { - assertUnfrozen('data', frozen) - namespace = key - return processor + assertUnfrozen('data', this.frozen) + this.namespace = key + return this } // Get space. - return namespace + return this.namespace } - /** @type {Processor['freeze']} */ - function freeze() { - if (frozen) { - return processor + /** + * Freeze a processor. + * + * Frozen processors are meant to be extended and not to be configured + * directly. + * + * When a processor is frozen it cannot be unfrozen. + * New processors working the same way can be created by calling the + * processor. + * + * It’s possible to freeze processors explicitly by calling `.freeze()`. + * Processors freeze automatically when `.parse()`, `.run()`, `.runSync()`, + * `.stringify()`, `.process()`, or `.processSync()` are called. + * + * @returns {Processor} + * The processor that `freeze` was called on. + */ + freeze() { + if (this.frozen) { + return this } - while (++freezeIndex < attachers.length) { - const [attacher, ...options] = attachers[freezeIndex] + // Cast so that we can type plugins easier. + // Plugins are supposed to be usable on different processors, not just on + // this exact processor. + const self = /** @type {Processor} */ (/** @type {unknown} */ (this)) + + while (++this.freezeIndex < this.attachers.length) { + const [attacher, ...options] = this.attachers[this.freezeIndex] if (options[0] === false) { continue @@ -129,29 +657,485 @@ function base() { options[0] = undefined } - const transformer = attacher.call(processor, ...options) + const transformer = attacher.call(self, ...options) if (typeof transformer === 'function') { - transformers.use(transformer) + this.transformers.use(transformer) } } - frozen = true - freezeIndex = Number.POSITIVE_INFINITY + this.frozen = true + this.freezeIndex = Number.POSITIVE_INFINITY + + return this + } + + /** + * Parse text to a syntax tree. + * + * > 👉 **Note**: `parse` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `parse` performs the parse phase, not the run phase or other + * > phases. + * + * @param {VFileCompatible | undefined} [file] + * file to parse; typically `string`; any value accepted as `x` in + * `new VFile(x)`. + * @returns {ParseTree extends undefined ? Node : ParseTree} + * Syntax tree representing `file`. + */ + parse(file) { + this.freeze() + const realFile = vfile(file) + const Parser = this.Parser + assertParser('parse', Parser) + + if (newable(Parser, 'parse')) { + const ParserClass = + /** @type {ParserClass} */ ( + Parser + ) + + const parserInstace = new ParserClass(String(realFile), realFile) + + return parserInstace.parse() + } + + const parserFunction = + /** @type {ParserFunction} */ ( + Parser + ) - return processor + return parserFunction(String(realFile), realFile) } /** - * @param {Exclude | PluggableList | null | undefined} [value] + * Process the given file as configured on the processor. + * + * > 👉 **Note**: `process` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `process` performs the parse, run, and stringify phases. + * + * @overload + * @param {VFileCompatible | undefined} file + * @param {ProcessCallback>} done + * @returns {undefined} + * + * @overload + * @param {VFileCompatible | undefined} [file] + * @returns {Promise>} + * + * @param {VFileCompatible | undefined} [file] + * File; any value accepted as `x` in `new VFile(x)`. + * @param {ProcessCallback> | undefined} [done] + * Callback. + * @returns {Promise | undefined} + * Nothing if `done` is given. + * Otherwise `Promise`, rejected with a fatal error or resolved with the + * processed file. + * + * The parsed, transformed, and compiled value is available at + * `file.value` (see note). + * + * > 👉 **Note**: unified typically compiles by serializing: most + * > compilers return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different + * > result values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react + */ + process(file, done) { + const self = this + this.freeze() + assertParser('process', this.Parser) + assertCompiler('process', this.Compiler) + + if (!done) { + return new Promise(executor) + } + + executor(undefined, done) + + // Note: `void`s needed for TS. + /** + * @param {((file: VFile) => undefined | void) | undefined} resolve + * @param {(error?: Error | undefined) => undefined | void} reject + * @returns {undefined} + */ + function executor(resolve, reject) { + const realFile = vfile(file) + // Assume `ParseTree` (the result of the parser) matches `HeadTree` (the + // input of the first transform). + const parseTree = + /** @type {HeadTree extends undefined ? Node : HeadTree} */ ( + /** @type {unknown} */ (self.parse(realFile)) + ) + + self.run(parseTree, realFile, function (error, tree, file) { + if (error || !tree || !file) { + realDone(error) + } else { + // Assume `TailTree` (the output of the last transform) matches + // `CompileTree` (the input of the compiler). + const compileTree = + /** @type {CompileTree extends undefined ? Node : CompileTree} */ ( + /** @type {unknown} */ (tree) + ) + + const result = self.stringify(compileTree, file) + + if (result === null || result === undefined) { + // Empty. + } else if (looksLikeAVFileValue(result)) { + file.value = result + } else { + file.result = result + } + + realDone(error, file) + } + }) + + /** + * @param {Error | undefined} [error] + * @param {VFile | undefined} [file] + * @returns {undefined} + */ + function realDone(error, file) { + if (error || !file) { + reject(error) + } else if (resolve) { + resolve(file) + } else { + // @ts-expect-error: `done` is defined if `resolve` is not. + done(undefined, file) + } + } + } + } + + /** + * Process the given file as configured on the processor. + * + * An error is thrown if asynchronous transforms are configured. + * + * > 👉 **Note**: `processSync` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `processSync` performs the parse, run, and stringify phases. + * + * @param {VFileCompatible | undefined} [file] + * File; any value accepted as `x` in `new VFile(x)`. + * @returns {VFileWithOutput} + * The processed file. + * + * The parsed, transformed, and compiled value is available at + * `file.value` (see note). + * + * > 👉 **Note**: unified typically compiles by serializing: most + * > compilers return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different + * > result values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react + */ + processSync(file) { + /** @type {boolean | undefined} */ + let complete + + this.freeze() + assertParser('processSync', this.Parser) + assertCompiler('processSync', this.Compiler) + + // The result will be set by `this.process` on this file. + const realFile = /** @type {VFileWithOutput} */ (vfile(file)) + + this.process(realFile, realDone) + + assertDone('processSync', 'process', complete) + + return realFile + + /** + * @param {Error | undefined} [error] + * @returns {undefined} + */ + function realDone(error) { + complete = true + bail(error) + } + } + + /** + * Run *transformers* on a syntax tree. + * + * > 👉 **Note**: `run` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `run` performs the run phase, not other phases. + * + * @overload + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * @param {RunCallback} done + * @returns {undefined} + * + * @overload + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * @param {VFileCompatible | undefined} file + * @param {RunCallback} done + * @returns {undefined} + * + * @overload + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * @param {VFileCompatible | undefined} [file] + * @returns {Promise} + * + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * Tree to transform and inspect. + * @param {( + * RunCallback | + * VFileCompatible + * )} [file] + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. + * @param {RunCallback} [done] + * @returns {Promise | undefined} + * Nothing if `done` is given. + * Otherwise, `Promise` rejected with a fatal error or resolved with the + * transformed tree. + */ + run(tree, file, done) { + assertNode(tree) + this.freeze() + + const transformers = this.transformers + + if (!done && typeof file === 'function') { + done = file + file = undefined + } + + if (!done) { + return new Promise(executor) + } + + executor(undefined, done) + + // Note: `void`s needed for TS. + /** + * @param {( + * ((tree: TailTree extends undefined ? Node : TailTree) => undefined | void) | + * undefined + * )} resolve + * @param {(error: Error) => undefined | void} reject + * @returns {undefined} + */ + function executor(resolve, reject) { + // @ts-expect-error: `file` can’t be a `done` anymore, we checked. + const realFile = vfile(file) + transformers.run(tree, realFile, realDone) + + /** + * @param {Error | undefined} error + * @param {Node} outputTree + * @param {VFile} file + * @returns {undefined} + */ + function realDone(error, outputTree, file) { + const resultingTree = + /** @type {TailTree extends undefined ? Node : TailTree} */ ( + outputTree || tree + ) + + if (error) { + reject(error) + } else if (resolve) { + resolve(resultingTree) + } else { + // @ts-expect-error: `done` is defined if `resolve` is not. + done(undefined, resultingTree, file) + } + } + } + } + + /** + * Run *transformers* on a syntax tree. + * + * An error is thrown if asynchronous transforms are configured. + * + * > 👉 **Note**: `runSync` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `runSync` performs the run phase, not other phases. + * + * @param {HeadTree extends undefined ? Node : HeadTree} tree + * Tree to transform and inspect. + * @param {VFileCompatible | undefined} [file] + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. + * @returns {TailTree extends undefined ? Node : TailTree} + * Transformed tree. + */ + runSync(tree, file) { + /** @type {Node | undefined} */ + let result + /** @type {boolean | undefined} */ + let complete + + this.run(tree, file, realDone) + + assertDone('runSync', 'run', complete) + + // @ts-expect-error: we either bailed on an error or have a tree. + return result + + /** + * @param {Error | undefined} [error] + * @param {Node} [tree] + * @returns {undefined} + */ + function realDone(error, tree) { + bail(error) + result = tree + complete = true + } + } + + /** + * Compile a syntax tree. + * + * > 👉 **Note**: `stringify` freezes the processor if not already *frozen*. + * + * > 👉 **Note**: `stringify` performs the stringify phase, not the run phase + * or other phases. + * + * @param {CompileTree extends undefined ? Node : CompileTree} tree + * Tree to compile + * @param {VFileCompatible | undefined} [file] + * File associated with `node` (optional); any value accepted as `x` in + * `new VFile(x)`. + * @returns {CompileResult extends undefined ? VFileValue | null : CompileResult} + * Textual representation of the tree (see note). + * + * > 👉 **Note**: unified typically compiles by serializing: most compilers + * > return `string` (or `Uint8Array`). + * > Some compilers, such as the one configured with + * > [`rehype-react`][rehype-react], return other values (in this case, a + * > React tree). + * > If you’re using a compiler that doesn’t serialize, expect different + * > result values. + * > + * > To register custom results in TypeScript, add them to + * > {@link CompileResultMap `CompileResultMap`}. + * + * [rehype-react]: https://github.com/rehypejs/rehype-react + */ + stringify(tree, file) { + this.freeze() + const realFile = vfile(file) + const Compiler = this.Compiler + assertCompiler('stringify', Compiler) + assertNode(tree) + + if (newable(Compiler, 'compile')) { + const CompilerClass = /** + * @type {( + * CompilerClass< + * CompileTree extends undefined ? Node : CompileTree, + * CompileResult extends undefined ? CompileResults : CompileResult + * > + * )} + */ (Compiler) + + const compilerInstace = new CompilerClass(tree, realFile) + + return compilerInstace.compile() + } + + const compilerFunction = /** + * @type {( + * CompilerFunction< + * CompileTree extends undefined ? Node : CompileTree, + * CompileResult extends undefined ? CompileResults : CompileResult + * > + * )} + */ (Compiler) + + return compilerFunction(tree, realFile) + } + + /** + * Configure the processor to use a plugin, a list of usable + * values, or with a preset. + * + * If the processor is already using a plugin, the previous plugin + * configuration is changed based on the options that are passed in. + * In other words, the plugin is not added a second time. + * + * @example + * ```js + * import {unified} from 'unified' + * + * unified() + * // Plugin with options: + * .use(pluginA, {x: true, y: true}) + * // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): + * .use(pluginA, {y: false, z: true}) + * // Plugins: + * .use([pluginB, pluginC]) + * // Two plugins, the second with options: + * .use([pluginD, [pluginE, {}]]) + * // Preset with plugins and settings: + * .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}}) + * // Settings only: + * .use({settings: {position: false}}) + * ``` + * + * @template {Array} [Parameters=[]] + * @template {Node | string | undefined} [Input=undefined] + * @template [Output=Input] + * + * @overload + * @param {Preset | null | undefined} [preset] + * @returns {Processor} + * + * @overload + * @param {PluggableList} list + * @returns {Processor} + * + * @overload + * @param {Plugin} plugin + * @param {...(Parameters | [boolean])} parameters + * @returns {UsePlugin} + * + * @overload + * @param {[plugin: Plugin, enable: boolean] | [plugin: Plugin, ...parameters: Parameters]} tuple + * @returns {UsePlugin} + * + * @param {[plugin: Plugin, ...parameters: Array] | PluggableList | Plugin | Preset | null | undefined} value + * Usable value. * @param {...unknown} options - * @returns {Processor} + * Parameters, when a plugin is given as a usable value. + * @returns {Processor} + * Current processor. */ - function use(value, ...options) { + use(value, ...options) { + const attachers = this.attachers + const namespace = this.namespace /** @type {Record | undefined} */ let settings - assertUnfrozen('use', frozen) + assertUnfrozen('use', this.frozen) if (value === null || value === undefined) { // Empty. @@ -159,6 +1143,7 @@ function base() { addPlugin(value, ...options) } else if (typeof value === 'object') { if (Array.isArray(value)) { + // @ts-expect-error: look at tuples? addList(value) } else { addPreset(value) @@ -172,10 +1157,10 @@ function base() { namespace.settings = Object.assign(namespace.settings || {}, settings) } - return processor + return this /** - * @param {import('../index.js').Pluggable} value + * @param {Pluggable} value * @returns {undefined} */ function add(value) { @@ -239,7 +1224,7 @@ function base() { */ function addPlugin(plugin, value) { let index = -1 - /** @type {Processor['attachers'][number] | undefined} */ + /** @type {PluginTuple> | undefined} */ let entry while (++index < attachers.length) { @@ -261,211 +1246,18 @@ function base() { } } } - - /** @type {Processor['parse']} */ - function parse(doc) { - processor.freeze() - const file = vfile(doc) - const Parser = processor.Parser - assertParser('parse', Parser) - - if (newable(Parser, 'parse')) { - // @ts-expect-error: `newable` checks this. - return new Parser(String(file), file).parse() - } - - // @ts-expect-error: `newable` checks this. - return Parser(String(file), file) // eslint-disable-line new-cap - } - - /** @type {Processor['stringify']} */ - function stringify(node, doc) { - processor.freeze() - const file = vfile(doc) - const Compiler = processor.Compiler - assertCompiler('stringify', Compiler) - assertNode(node) - - if (newable(Compiler, 'compile')) { - // @ts-expect-error: `newable` checks this. - return new Compiler(node, file).compile() - } - - // @ts-expect-error: `newable` checks this. - return Compiler(node, file) // eslint-disable-line new-cap - } - - /** - * @param {Node} node - * @param {RunCallback | VFileCompatible} [doc] - * @param {RunCallback} [callback] - * @returns {Promise | undefined} - */ - function run(node, doc, callback) { - assertNode(node) - processor.freeze() - - if (!callback && typeof doc === 'function') { - callback = doc - doc = undefined - } - - if (!callback) { - return new Promise(executor) - } - - executor(undefined, callback) - - // Note: `void`s needed for TS. - /** - * @param {((node: Node) => undefined | void) | undefined} resolve - * @param {(error: Error) => undefined | void} reject - * @returns {undefined} - */ - function executor(resolve, reject) { - // @ts-expect-error: `doc` can’t be a callback anymore, we checked. - transformers.run(node, vfile(doc), done) - - /** - * @param {Error | undefined} error - * @param {Node} tree - * @param {VFile} file - * @returns {undefined} - */ - function done(error, tree, file) { - tree = tree || node - if (error) { - reject(error) - } else if (resolve) { - resolve(tree) - } else { - // @ts-expect-error: `callback` is defined if `resolve` is not. - callback(undefined, tree, file) - } - } - } - } - - /** @type {Processor['runSync']} */ - function runSync(node, file) { - /** @type {Node | undefined} */ - let result - /** @type {boolean | undefined} */ - let complete - - processor.run(node, file, done) - - assertDone('runSync', 'run', complete) - - // @ts-expect-error: we either bailed on an error or have a tree. - return result - - /** - * @param {Error | undefined} [error] - * @param {Node} [tree] - * @returns {undefined} - */ - function done(error, tree) { - bail(error) - result = tree - complete = true - } - } - - /** - * @param {VFileCompatible} doc - * @param {ProcessCallback} [callback] - * @returns {Promise | undefined} - */ - function process(doc, callback) { - processor.freeze() - assertParser('process', processor.Parser) - assertCompiler('process', processor.Compiler) - - if (!callback) { - return new Promise(executor) - } - - executor(undefined, callback) - - // Note: `void`s needed for TS. - /** - * @param {((file: VFile) => undefined | void) | undefined} resolve - * @param {(error?: Error | undefined) => undefined | void} reject - * @returns {undefined} - */ - function executor(resolve, reject) { - const file = vfile(doc) - - processor.run(processor.parse(file), file, function (error, tree, file) { - if (error || !tree || !file) { - done(error) - } else { - /** @type {unknown} */ - const result = processor.stringify(tree, file) - - if (result === null || result === undefined) { - // Empty. - } else if (looksLikeAVFileValue(result)) { - file.value = result - } else { - file.result = result - } - - done(error, file) - } - }) - - /** - * @param {Error | undefined} [error] - * @param {VFile | undefined} [file] - * @returns {undefined} - */ - function done(error, file) { - if (error || !file) { - reject(error) - } else if (resolve) { - resolve(file) - } else { - // @ts-expect-error: `callback` is defined if `resolve` is not. - callback(undefined, file) - } - } - } - } - - /** @type {import('../index.js').Processor['processSync']} */ - function processSync(doc) { - /** @type {boolean | undefined} */ - let complete - - processor.freeze() - assertParser('processSync', processor.Parser) - assertCompiler('processSync', processor.Compiler) - - const file = vfile(doc) - - processor.process(file, done) - - assertDone('processSync', 'process', complete) - - return file - - /** - * @param {Error | undefined} [error] - * @returns {undefined} - */ - function done(error) { - complete = true - bail(error) - } - } } +/** + * Base processor. + */ +export const unified = new Processor().freeze() + /** * Check if `value` is a constructor. * - * @param {unknown} value + * @template {unknown} Value + * @param {Value} value * @param {string} name * @returns {boolean} */ @@ -520,7 +1312,7 @@ function assertParser(name, value) { * * @param {string} name * @param {unknown} value - * @returns {asserts value is Compiler} + * @returns {asserts value is Compiler} */ function assertCompiler(name, value) { if (typeof value !== 'function') { diff --git a/package.json b/package.json index e135f064..a51d02db 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "xo": "^0.56.0" }, "scripts": { - "build": "tsc --build --clean && tsc --build && type-coverage && tsd", + "build": "tsc --build --clean && tsc --build && node script/fix-types.js && type-coverage && tsd", "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", "prepack": "npm run build && npm run format", "test": "npm run build && npm run format && npm run test-coverage", diff --git a/script/fix-types.js b/script/fix-types.js new file mode 100644 index 00000000..a17b1fcf --- /dev/null +++ b/script/fix-types.js @@ -0,0 +1,38 @@ +import fs from 'node:fs/promises' + +const url = new URL('../lib/index.d.ts', import.meta.url) + +let file = '' + +try { + file = String(await fs.readFile(url)) +} catch { + console.error('Could not read `lib/index.d.ts`, did `tsc` run already?') +} + +const result = file + .replace(/declare const Processor_base: [^\n]+/, function () { + console.log('Fixed `CallableInstance` import') + return "declare const CallableInstance: import('./callable-instance.js').ICallableInstance" + }) + .replace(/extends Processor_base/, function () { + console.log('Fixed `CallableInstance` use') + return 'extends CallableInstance<[], Processor>' + }) + .replace( + /\.\.\.parameters: Parameters_1 \| \[boolean] \| undefined/, + function () { + console.log( + 'Fixed `use` overload with plugin, and *non-optional* parameters' + ) + return '...parameters: Parameters_1 | [boolean]' + } + ) + +if (file === result) { + console.error( + 'Could not fix `lib/index.d.ts`, was `tsc` fixed somewhow? Or were changes already applied?' + ) +} else { + await fs.writeFile(url, result) +} diff --git a/test/core.js b/test/core.js index 5fc9ffda..b9e056ba 100644 --- a/test/core.js +++ b/test/core.js @@ -9,7 +9,6 @@ test('core', async function (t) { await t.test('should expose a frozen processor', async function () { assert.throws(function () { - // @ts-expect-error: check that `use` cannot be used on frozen processors. unified.use(function () {}) }, /Cannot call `use` on a frozen processor/) }) diff --git a/test/parse.js b/test/parse.js index 3fa472f2..1d4dc5e6 100644 --- a/test/parse.js +++ b/test/parse.js @@ -73,17 +73,28 @@ test('`parse`', async function (t) { async function () { const processor = unified() - processor.Parser = function (doc, file) { + /** + * @constructor + * @param {string} doc + * @param {VFile} file + */ + function Parser(doc, file) { assert.equal(typeof doc, 'string') assert.ok(file instanceof VFile) assert.equal(arguments.length, 2) } - processor.Parser.prototype.parse = function () { + /** + * @returns {Node} + */ + // type-coverage:ignore-next-line -- for some reason TS does understand `Parser.prototype`, but not `Compiler.prototype`. + Parser.prototype.parse = function () { assert.equal(arguments.length, 0) return givenNode } + processor.Parser = Parser + assert.equal(processor.parse('charlie'), givenNode) } ) diff --git a/test/process-compilers.js b/test/process-compilers.js index dad6c4c1..88c89d56 100644 --- a/test/process-compilers.js +++ b/test/process-compilers.js @@ -59,6 +59,8 @@ test('process (compilers)', async function (t) { } processor.Parser = simpleParser + + // @ts-expect-error: custom node, which should be registered!. processor.Compiler = function () { return result } diff --git a/test/stringify.js b/test/stringify.js index ad86b19d..bc55efee 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -70,18 +70,25 @@ test('`stringify`', async function (t) { async function () { const processor = unified() - processor.Compiler = function (node, file) { + /** + * @constructor + * @param {Node} node + * @param {VFile} file + */ + function Compiler(node, file) { assert.equal(node, givenNode, 'should pass a node') assert.ok(file instanceof VFile, 'should pass a file') assert.equal(arguments.length, 2) } - // type-coverage:ignore-next-line -- for some reason TS does understand `Parser.prototype`, but not `Compiler.prototype`. - processor.Compiler.prototype.compile = function () { + // type-coverage:ignore-next-line -- for some reason TS does understand `Compiler.prototype`, but not `Compiler.prototype`. + Compiler.prototype.compile = function () { assert.equal(arguments.length, 0) return 'echo' } + processor.Compiler = Compiler + assert.equal(processor.stringify(givenNode, givenFile), 'echo') } ) diff --git a/tsconfig.json b/tsconfig.json index ad1496e9..dd290ca4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "target": "es2020" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "index.d.ts"] + "include": ["**/*.js", "lib/callable-instance.d.ts", "index.d.ts"] } From e58b095bbea800764d447c00196f3d09ad44c107 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 12:38:54 +0200 Subject: [PATCH 34/46] Refactor code-style * Remove types for actually unsupported tuple support in `use`, tuples can exist in lists, but not at the top * Add more info to JSDoc comments * Use more casts, add development assertions, remove `ts-expect-error`s --- index.test-d.ts | 3 - lib/callable-instance.js | 1 - lib/index.js | 243 ++++++++++++++++++-------------------- package.json | 6 +- readme.md | 2 +- test/process-compilers.js | 8 +- 6 files changed, 128 insertions(+), 135 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index 2523938e..442c3295 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -162,7 +162,6 @@ unified().use(pluginWithSeveralArgumentsImplicit, {example: ''}, 1) unified() .use(pluginWithOptions, {example: ''}) - .use([pluginWithOptions, {example: ''}]) .use([[pluginWithOptions, {example: ''}]]) .use({ plugins: [[pluginWithOptions, {example: ''}]] @@ -173,8 +172,6 @@ unified() unified() .use(pluginWithoutOptions, true) .use(pluginWithoutOptions, false) - .use([pluginWithoutOptions, true]) - .use([pluginWithoutOptions, false]) .use([ [pluginWithoutOptions, true], [pluginWithoutOptions, false] diff --git a/lib/callable-instance.js b/lib/callable-instance.js index 6c092d88..e42eb011 100644 --- a/lib/callable-instance.js +++ b/lib/callable-instance.js @@ -1,4 +1,3 @@ -/* eslint-disable unicorn/no-this-assignment */ /** * @param {string} property */ diff --git a/lib/index.js b/lib/index.js index f81bc3bd..f51374db 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,3 @@ -/* eslint-disable unicorn/no-this-assignment */ /** * @typedef {import('trough').Pipeline} Pipeline * @@ -20,9 +19,9 @@ /** * @template {Node} [Tree=Node] - * The node that the compiler receives. + * The node that the compiler receives (default: `Node`). * @template {CompileResults} [Result=CompileResults] - * The thing that the compiler yields. + * The thing that the compiler yields (default: `CompileResults`). * @typedef {CompilerClass | CompilerFunction} Compiler * A **compiler** handles the compiling of a syntax tree to something else * (in most cases, text). @@ -54,7 +53,9 @@ /** * @template {Node} [Tree=Node] + * The node that the compiler receives (default: `Node`) * @template {CompileResults} [Result=CompileResults] + * The thing that the compiler yields (default: `CompileResults`). * @typedef {({ * prototype: {compile(): Result} * new (tree: Tree, file: VFile): CompilerClass['prototype'] @@ -64,9 +65,9 @@ /** * @template {Node} [Tree=Node] - * The node that the compiler receives. + * The node that the compiler receives (default: `Node`). * @template {CompileResults} [Result=CompileResults] - * The thing that the compiler yields. + * The thing that the compiler yields (default: `CompileResults`). * @callback CompilerFunction * Regular function to compile a tree. * @param {Tree} tree @@ -80,7 +81,7 @@ /** * @template {Node} [Tree=Node] - * The node that the parser yields. + * The node that the parser yields (default: `Node`) * @typedef {ParserClass | ParserFunction} Parser * A **parser** handles the parsing of text to a syntax tree. * @@ -98,6 +99,7 @@ /** * @template {Node} [Tree=Node] + * The node that the parser yields (default: `Node`). * @typedef {({ * prototype: {parse(): Tree} * new (document: string, file: VFile): ParserClass['prototype'] @@ -107,7 +109,7 @@ /** * @template {Node} [Tree=Node] - * The node that the parser yields. + * The node that the parser yields (default: `Node`). * @callback ParserFunction * Regular function to parse a file. * @param {string} document @@ -136,9 +138,9 @@ // . /** * @template {Array} [PluginParameters=[]] - * Arguments passed to the plugin. + * Arguments passed to the plugin (default: `[]`, the empty tuple). * @template {Node | string | undefined} [Input=Node] - * Value that is expected as input. + * Value that is expected as input (default: `Node`). * * * If the plugin returns a {@link Transformer `Transformer`}, this * should be the node it expects. @@ -147,7 +149,7 @@ * * If the plugin sets a {@link Compiler `Compiler`}, this should be the * node it expects. * @template [Output=Input] - * Value that is yielded as output. + * Value that is yielded as output (default: `Input`). * * * If the plugin returns a {@link Transformer `Transformer`}, this * should be the node that that yields. @@ -191,9 +193,9 @@ * The first item is a plugin, the rest are its parameters. * * @template {Array} [TupleParameters=[]] - * Arguments passed to the plugin. + * Arguments passed to the plugin (default: `[]`, the empty tuple). * @template {Node | string | undefined} [Input=undefined] - * Value that is expected as input. + * Value that is expected as input (optional). * * * If the plugin returns a {@link Transformer `Transformer`}, this * should be the node it expects. @@ -201,7 +203,7 @@ * `string`. * * If the plugin sets a {@link Compiler `Compiler`}, this should be the * node it expects. - * @template [Output=undefined] + * @template [Output=undefined] (optional). * @typedef {( * [ * plugin: Plugin, @@ -224,48 +226,48 @@ * * They can contain plugins and settings. * @property {PluggableList | undefined} [plugins] - * List of plugins and presets. + * List of plugins and presets (optional). * @property {Record | undefined} [settings] - * Shared settings for parsers and compilers. + * Shared settings for parsers and compilers (optional). */ /** * @template {VFile} [File=VFile] - * The file that the callback receives. + * The file that the callback receives (default: `VFile`). * @callback ProcessCallback * Callback called when the process is done. * * Called with either an error or a result. * @param {Error | undefined} [error] - * Fatal error. + * Fatal error (optional). * @param {File | undefined} [file] - * Processed file. + * Processed file (optional). * @returns {undefined} * Nothing. */ /** * @template {Node} [Tree=Node] - * The tree that the callback receives. + * The tree that the callback receives (default: `Node`). * @callback RunCallback * Callback called when transformers are done. * * Called with either an error or results. * @param {Error | undefined} [error] - * Fatal error. + * Fatal error (optional). * @param {Tree | undefined} [tree] - * Transformed tree. + * Transformed tree (optional). * @param {VFile | undefined} [file] - * File. + * File (optional). * @returns {undefined} * Nothing. */ /** * @template {Node} [Input=Node] - * Node type that the transformer expects. + * Node type that the transformer expects (default: `Node`). * @template {Node} [Output=Input] - * Node type that the transformer yields. + * Node type that the transformer yields (default: `Input`). * @callback Transformer * Transformers handle syntax trees and files. * @@ -303,7 +305,7 @@ /** * @template {Node} [Output=Node] - * Node type that the transformer yields. + * Node type that the transformer yields (default: `Node`). * @callback TransformCallback * If the signature of a `transformer` accepts a third argument, the * transformer may perform asynchronous operations, and must call `next()`. @@ -331,7 +333,7 @@ * @template {Node | string | undefined} Input * Input of plugin. * @template Output - * Output of plugin. + * Output of plugin (optional). * @typedef {( * Input extends string * ? Output extends Node | undefined @@ -391,6 +393,7 @@ import structuredClone from '@ungap/structured-clone' import {bail} from 'bail' +import {ok as assert} from 'devlop' import isPlainObj from 'is-plain-obj' import {trough} from 'trough' import {VFile} from 'vfile' @@ -400,15 +403,15 @@ const own = {}.hasOwnProperty /** * @template {Node | undefined} [ParseTree=undefined] - * Output of `parse`. + * Output of `parse` (optional). * @template {Node | undefined} [HeadTree=undefined] - * Input for `run`. + * Input for `run` (optional). * @template {Node | undefined} [TailTree=undefined] - * Output for `run`. + * Output for `run` (optional). * @template {Node | undefined} [CompileTree=undefined] - * Input of `stringify`. + * Input of `stringify` (optional). * @template {CompileResults | undefined} [CompileResult=undefined] - * Output of `stringify`. + * Output of `stringify` (optional). * @extends {CallableInstance<[], Processor>} */ export class Processor extends CallableInstance { @@ -588,9 +591,9 @@ export class Processor extends CallableInstance { * * @param {Record | string} [key] * Key to get or set, or entire dataset to set, or nothing to get the - * entire dataset. + * entire dataset (optional). * @param {unknown} [value] - * Value to set. + * Value to set (optional). * @returns {unknown} * The processor that `data` is called on when settings, the value at `key` * when getting, or the entire dataset when getting w/o key. @@ -679,8 +682,8 @@ export class Processor extends CallableInstance { * > phases. * * @param {VFileCompatible | undefined} [file] - * file to parse; typically `string`; any value accepted as `x` in - * `new VFile(x)`. + * file to parse (optional); typically `string`; any value accepted as `x` + * in `new VFile(x)`. * @returns {ParseTree extends undefined ? Node : ParseTree} * Syntax tree representing `file`. */ @@ -726,9 +729,9 @@ export class Processor extends CallableInstance { * @returns {Promise>} * * @param {VFileCompatible | undefined} [file] - * File; any value accepted as `x` in `new VFile(x)`. + * File (optional); any value accepted as `x` in `new VFile(x)`. * @param {ProcessCallback> | undefined} [done] - * Callback. + * Callback (optional). * @returns {Promise | undefined} * Nothing if `done` is given. * Otherwise `Promise`, rejected with a fatal error or resolved with the @@ -752,20 +755,17 @@ export class Processor extends CallableInstance { */ process(file, done) { const self = this + this.freeze() assertParser('process', this.Parser) assertCompiler('process', this.Compiler) - if (!done) { - return new Promise(executor) - } - - executor(undefined, done) + return done ? executor(undefined, done) : new Promise(executor) // Note: `void`s needed for TS. /** - * @param {((file: VFile) => undefined | void) | undefined} resolve - * @param {(error?: Error | undefined) => undefined | void} reject + * @param {((file: VFileWithOutput) => undefined | void) | undefined} resolve + * @param {(error: Error | undefined) => undefined | void} reject * @returns {undefined} */ function executor(resolve, reject) { @@ -779,32 +779,32 @@ export class Processor extends CallableInstance { self.run(parseTree, realFile, function (error, tree, file) { if (error || !tree || !file) { - realDone(error) + return realDone(error) + } + + // Assume `TailTree` (the output of the last transform) matches + // `CompileTree` (the input of the compiler). + const compileTree = + /** @type {CompileTree extends undefined ? Node : CompileTree} */ ( + /** @type {unknown} */ (tree) + ) + + const compileResult = self.stringify(compileTree, file) + + if (compileResult === null || compileResult === undefined) { + // Empty. + } else if (looksLikeAVFileValue(compileResult)) { + file.value = compileResult } else { - // Assume `TailTree` (the output of the last transform) matches - // `CompileTree` (the input of the compiler). - const compileTree = - /** @type {CompileTree extends undefined ? Node : CompileTree} */ ( - /** @type {unknown} */ (tree) - ) - - const result = self.stringify(compileTree, file) - - if (result === null || result === undefined) { - // Empty. - } else if (looksLikeAVFileValue(result)) { - file.value = result - } else { - file.result = result - } - - realDone(error, file) + file.result = compileResult } + + realDone(error, /** @type {VFileWithOutput} */ (file)) }) /** - * @param {Error | undefined} [error] - * @param {VFile | undefined} [file] + * @param {Error | undefined} error + * @param {VFileWithOutput | undefined} [file] * @returns {undefined} */ function realDone(error, file) { @@ -813,7 +813,7 @@ export class Processor extends CallableInstance { } else if (resolve) { resolve(file) } else { - // @ts-expect-error: `done` is defined if `resolve` is not. + assert(done, '`done` is defined if `resolve` is not') done(undefined, file) } } @@ -830,7 +830,7 @@ export class Processor extends CallableInstance { * > 👉 **Note**: `processSync` performs the parse, run, and stringify phases. * * @param {VFileCompatible | undefined} [file] - * File; any value accepted as `x` in `new VFile(x)`. + * File (optional); any value accepted as `x` in `new VFile(x)`. * @returns {VFileWithOutput} * The processed file. * @@ -851,29 +851,28 @@ export class Processor extends CallableInstance { * [rehype-react]: https://github.com/rehypejs/rehype-react */ processSync(file) { - /** @type {boolean | undefined} */ - let complete + /** @type {boolean} */ + let complete = false + /** @type {VFileWithOutput | undefined} */ + let result this.freeze() assertParser('processSync', this.Parser) assertCompiler('processSync', this.Compiler) - // The result will be set by `this.process` on this file. - const realFile = /** @type {VFileWithOutput} */ (vfile(file)) - - this.process(realFile, realDone) - + this.process(file, realDone) assertDone('processSync', 'process', complete) + assert(result, 'we either bailed on an error or have a tree') - return realFile + return result /** - * @param {Error | undefined} [error] - * @returns {undefined} + * @type {ProcessCallback>} */ - function realDone(error) { + function realDone(error, file) { complete = true bail(error) + result = file } } @@ -909,6 +908,7 @@ export class Processor extends CallableInstance { * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. * @param {RunCallback} [done] + * Callback (optional). * @returns {Promise | undefined} * Nothing if `done` is given. * Otherwise, `Promise` rejected with a fatal error or resolved with the @@ -925,11 +925,7 @@ export class Processor extends CallableInstance { file = undefined } - if (!done) { - return new Promise(executor) - } - - executor(undefined, done) + return done ? executor(undefined, done) : new Promise(executor) // Note: `void`s needed for TS. /** @@ -941,7 +937,10 @@ export class Processor extends CallableInstance { * @returns {undefined} */ function executor(resolve, reject) { - // @ts-expect-error: `file` can’t be a `done` anymore, we checked. + assert( + typeof file !== 'function', + '`file` can’t be a `done` anymore, we checked' + ) const realFile = vfile(file) transformers.run(tree, realFile, realDone) @@ -962,7 +961,7 @@ export class Processor extends CallableInstance { } else if (resolve) { resolve(resultingTree) } else { - // @ts-expect-error: `done` is defined if `resolve` is not. + assert(done, '`done` is defined if `resolve` is not') done(undefined, resultingTree, file) } } @@ -987,22 +986,19 @@ export class Processor extends CallableInstance { * Transformed tree. */ runSync(tree, file) { - /** @type {Node | undefined} */ + /** @type {boolean} */ + let complete = false + /** @type {(TailTree extends undefined ? Node : TailTree) | undefined} */ let result - /** @type {boolean | undefined} */ - let complete this.run(tree, file, realDone) assertDone('runSync', 'run', complete) - - // @ts-expect-error: we either bailed on an error or have a tree. + assert(result, 'we either bailed on an error or have a tree') return result /** - * @param {Error | undefined} [error] - * @param {Node} [tree] - * @returns {undefined} + * @type {RunCallback} */ function realDone(error, tree) { bail(error) @@ -1118,18 +1114,14 @@ export class Processor extends CallableInstance { * @param {...(Parameters | [boolean])} parameters * @returns {UsePlugin} * - * @overload - * @param {[plugin: Plugin, enable: boolean] | [plugin: Plugin, ...parameters: Parameters]} tuple - * @returns {UsePlugin} - * - * @param {[plugin: Plugin, ...parameters: Array] | PluggableList | Plugin | Preset | null | undefined} value + * @param {PluggableList | Plugin | Preset | null | undefined} value * Usable value. - * @param {...unknown} options + * @param {...unknown} parameters * Parameters, when a plugin is given as a usable value. * @returns {Processor} * Current processor. */ - use(value, ...options) { + use(value, ...parameters) { const attachers = this.attachers const namespace = this.namespace /** @type {Record | undefined} */ @@ -1140,10 +1132,9 @@ export class Processor extends CallableInstance { if (value === null || value === undefined) { // Empty. } else if (typeof value === 'function') { - addPlugin(value, ...options) + addPlugin(value, parameters) } else if (typeof value === 'object') { if (Array.isArray(value)) { - // @ts-expect-error: look at tuples? addList(value) } else { addPreset(value) @@ -1165,12 +1156,12 @@ export class Processor extends CallableInstance { */ function add(value) { if (typeof value === 'function') { - addPlugin(value) + addPlugin(value, []) } else if (typeof value === 'object') { if (Array.isArray(value)) { - const [plugin, ...options] = - /** @type {[Plugin, ...Array]} */ (value) - addPlugin(plugin, ...options) + const [plugin, ...parameters] = + /** @type {PluginTuple>} */ (value) + addPlugin(plugin, parameters) } else { addPreset(value) } @@ -1199,7 +1190,7 @@ export class Processor extends CallableInstance { } /** - * @param {PluggableList | null | undefined} [plugins] + * @param {PluggableList | null | undefined} plugins * @returns {undefined} */ function addList(plugins) { @@ -1219,10 +1210,10 @@ export class Processor extends CallableInstance { /** * @param {Plugin} plugin - * @param {...unknown} [value] + * @param {Array} parameters * @returns {undefined} */ - function addPlugin(plugin, value) { + function addPlugin(plugin, parameters) { let index = -1 /** @type {PluginTuple> | undefined} */ let entry @@ -1235,14 +1226,18 @@ export class Processor extends CallableInstance { } if (entry) { + let value = parameters[0] if (isPlainObj(entry[1]) && isPlainObj(value)) { value = structuredClone({...entry[1], ...value}) } entry[1] = value + // To do: should the rest be set? } else { - // @ts-expect-error: fine. - attachers.push([...arguments]) + // It’s important to pass arguments, because an explicit passed + // `undefined` is different from a not-passed `value`. + // This way we keep things at their place. + attachers.push([plugin, ...parameters]) } } } @@ -1256,29 +1251,27 @@ export const unified = new Processor().freeze() /** * Check if `value` is a constructor. * - * @template {unknown} Value - * @param {Value} value + * @param {unknown} value * @param {string} name * @returns {boolean} */ function newable(value, name) { - return ( - typeof value === 'function' && - // Prototypes do exist. - // type-coverage:ignore-next-line - value.prototype && - // A function with keys in its prototype is probably a constructor. - // Classes’ prototype methods are not enumerable, so we check if some value - // exists in the prototype. + const proto = + // Prototypes are `unknown`. // type-coverage:ignore-next-line - (keys(value.prototype) || name in value.prototype) + typeof value === 'function' && /** @type {unknown} */ (value.prototype) + + return ( + proto !== null && + typeof proto === 'object' && + (keys(proto) || name in proto) ) } /** * Check if `value` is an object with keys. * - * @param {Record} value + * @param {object} value * @returns {boolean} */ function keys(value) { @@ -1369,7 +1362,7 @@ function assertDone(name, asyncName, complete) { } /** - * @param {VFileCompatible} [value] + * @param {VFileCompatible | undefined} [value] * @returns {VFile} */ function vfile(value) { @@ -1377,7 +1370,7 @@ function vfile(value) { } /** - * @param {VFileCompatible} [value] + * @param {VFileCompatible | undefined} [value] * @returns {value is VFile} */ function looksLikeAVFile(value) { diff --git a/package.json b/package.json index a51d02db..235a4bb8 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "bail": "^2.0.0", + "devlop": "^1.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" @@ -110,6 +111,9 @@ } } ], - "prettier": true + "prettier": true, + "rules": { + "unicorn/no-this-assignment": "off" + } } } diff --git a/readme.md b/readme.md index 6ac7a007..ce24b8ae 100644 --- a/readme.md +++ b/readme.md @@ -1261,7 +1261,7 @@ node types for the syntax trees provided by our packages (as in, * @typedef Options * Configuration (optional). * @property {boolean} [someField] - * Some option. + * Some option (optional). */ // To type options: diff --git a/test/process-compilers.js b/test/process-compilers.js index 88c89d56..716fbbdf 100644 --- a/test/process-compilers.js +++ b/test/process-compilers.js @@ -52,15 +52,15 @@ test('process (compilers)', async function (t) { const processor = unified() const result = { _owner: null, - type: 'p', - ref: null, key: 'h-1', - props: {children: ['bravo']} + props: {children: ['bravo']}, + ref: null, + type: 'p' } processor.Parser = simpleParser - // @ts-expect-error: custom node, which should be registered!. + // @ts-expect-error: custom result, which should be registered! processor.Compiler = function () { return result } From f3e71a8f67d87b836f3f4219928b9095d0078722 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 12:39:47 +0200 Subject: [PATCH 35/46] Remove `Attacher` type --- index.d.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6a4f5855..713a6b3d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,8 +10,6 @@ export type { Pluggable, PluggableList, Plugin, - // To do: remove next major. - Plugin as Attacher, PluginTuple, Parser, ParserClass, From 6f068a019115cdcf73dd13b53ac45d65f9d6bf0f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 12:45:31 +0200 Subject: [PATCH 36/46] Fix to deep clone preset settings --- lib/index.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/index.js b/lib/index.js index f51374db..efb95eb4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1124,8 +1124,6 @@ export class Processor extends CallableInstance { use(value, ...parameters) { const attachers = this.attachers const namespace = this.namespace - /** @type {Record | undefined} */ - let settings assertUnfrozen('use', this.frozen) @@ -1143,11 +1141,6 @@ export class Processor extends CallableInstance { throw new TypeError('Expected usable value, not `' + value + '`') } - if (settings) { - // To do: structured clone? - namespace.settings = Object.assign(namespace.settings || {}, settings) - } - return this /** @@ -1184,8 +1177,10 @@ export class Processor extends CallableInstance { addList(result.plugins) if (result.settings) { - // To do: structured clone? - settings = Object.assign(settings || {}, result.settings) + namespace.settings = { + ...namespace.settings, + ...structuredClone(result.settings) + } } } From 56ee288d34466fd0e231ed70334992481d982a9a Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 13:03:14 +0200 Subject: [PATCH 37/46] Fix non-first parameter merging when reconfiguring plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you configure a plugin with multiple parameters (which is supported but not particularly recommended, or used), and then reconfigure it multiple times, the non-first parameters (so everything other than what’s typically the `options` object), we now take the last configured values. Previously, earlier values remained. --- lib/index.js | 27 +++++++++--------- test/use.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/lib/index.js b/lib/index.js index efb95eb4..71c3cff6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1210,29 +1210,28 @@ export class Processor extends CallableInstance { */ function addPlugin(plugin, parameters) { let index = -1 - /** @type {PluginTuple> | undefined} */ - let entry + let entryIndex = -1 while (++index < attachers.length) { if (attachers[index][0] === plugin) { - entry = attachers[index] + entryIndex = index break } } - if (entry) { - let value = parameters[0] - if (isPlainObj(entry[1]) && isPlainObj(value)) { - value = structuredClone({...entry[1], ...value}) + if (entryIndex === -1) { + attachers.push([plugin, ...parameters]) + } + // Only set if there was at least a `primary` value, otherwise we’d change + // `arguments.length`. + else if (parameters.length > 0) { + let [primary, ...rest] = parameters + const currentPrimary = attachers[entryIndex][1] + if (isPlainObj(currentPrimary) && isPlainObj(primary)) { + primary = structuredClone({...currentPrimary, ...primary}) } - entry[1] = value - // To do: should the rest be set? - } else { - // It’s important to pass arguments, because an explicit passed - // `undefined` is different from a not-passed `value`. - // This way we keep things at their place. - attachers.push([plugin, ...parameters]) + attachers[entryIndex] = [plugin, primary, ...rest] } } } diff --git a/test/use.js b/test/use.js index 4f0f4523..77e00b54 100644 --- a/test/use.js +++ b/test/use.js @@ -1291,4 +1291,83 @@ test('`use`', async function (t) { } ) }) + + await t.test('reconfigure (non-first parameters)', async function (t) { + await t.test( + 'should reconfigure plugins (non-first parameters)', + async function () { + let calls = 0 + + unified() + .use(fn, givenOptions, givenOptions, givenOptions, undefined) + .use(fn, otherOptions, otherOptions, undefined, otherOptions) + .freeze() + + assert.equal(calls, 1) + + /** + * @param {unknown} a + * @param {unknown} b + * @param {unknown} c + * @param {unknown} d + */ + function fn(a, b, c, d) { + assert.equal(arguments.length, 4) + assert.deepEqual(a, mergedOptions) + assert.deepEqual(b, otherOptions) + assert.deepEqual(c, undefined) + assert.deepEqual(d, otherOptions) + calls++ + } + } + ) + + await t.test('should keep parameter length (#1)', async function () { + let calls = 0 + + unified().use(fn).use(fn).freeze() + + assert.equal(calls, 1) + + /** + * @param {...unknown} parameters + */ + function fn(...parameters) { + assert.deepEqual(parameters, []) + calls++ + } + }) + + await t.test('should keep parameter length (#2)', async function () { + let calls = 0 + + unified().use(fn, givenOptions).use(fn).freeze() + + assert.equal(calls, 1) + + /** + * @param {...unknown} parameters + */ + function fn(...parameters) { + assert.deepEqual(parameters, [givenOptions]) + calls++ + } + }) + + await t.test('should keep parameter length (#3)', async function () { + let calls = 0 + + unified().use(fn).use(fn, givenOptions).freeze() + + assert.equal(calls, 1) + + /** + * @param {...unknown} parameters + */ + function fn(...parameters) { + assert.deepEqual(parameters, [givenOptions]) + calls++ + } + }) + }) }) From 4676814d49e483e446e86c882d583694c4177ff7 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 13:11:12 +0200 Subject: [PATCH 38/46] Remove support for compilers returning nullish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You’re supposed to compile to something. --- index.d.ts | 2 -- index.test-d.ts | 28 +++++++++------------------- lib/index.js | 6 ++---- test/process-compilers.js | 14 -------------- 4 files changed, 11 insertions(+), 39 deletions(-) diff --git a/index.d.ts b/index.d.ts index 713a6b3d..ffb08843 100644 --- a/index.d.ts +++ b/index.d.ts @@ -48,6 +48,4 @@ export interface CompileResultMap { // Note: if `Value` from `VFile` is changed, this should too. Uint8Array: Uint8Array string: string - // Empties. - null: null } diff --git a/index.test-d.ts b/index.test-d.ts index 442c3295..8a3f45f7 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -343,9 +343,7 @@ expectType>(processorWithRemarkParse) expectType(processorWithRemarkParse.parse('')) expectType(processorWithRemarkParse.runSync(mdastRoot)) expectType(processorWithRemarkParse.runSync(hastRoot)) -expectType( - processorWithRemarkParse.stringify(mdastRoot) -) +expectType(processorWithRemarkParse.stringify(mdastRoot)) processorWithRemarkParse.stringify(hastRoot) expectType(processorWithRemarkParse.processSync('')) @@ -361,12 +359,8 @@ expectType(processorWithRemarkLint.parse('')) expectType(processorWithRemarkLint.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkLint.runSync(hastRoot) -expectType( - processorWithRemarkLint.stringify(mdastRoot) -) -expectType( - processorWithRemarkLint.stringify(hastRoot) -) +expectType(processorWithRemarkLint.stringify(mdastRoot)) +expectType(processorWithRemarkLint.stringify(hastRoot)) expectType(processorWithRemarkLint.processSync('')) // Inspect/transform plugin (implicit). @@ -386,10 +380,10 @@ expectType(processorWithRemarkLintImplicit.parse('')) expectType(processorWithRemarkLintImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkLintImplicit.runSync(hastRoot) -expectType( +expectType( processorWithRemarkLintImplicit.stringify(mdastRoot) ) -expectType( +expectType( processorWithRemarkLintImplicit.stringify(hastRoot) ) expectType(processorWithRemarkLintImplicit.processSync('')) @@ -406,12 +400,8 @@ expectType(processorWithRemarkRehype.parse('')) expectType(processorWithRemarkRehype.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehype.runSync(hastRoot) -expectType( - processorWithRemarkRehype.stringify(hastRoot) -) -expectType( - processorWithRemarkRehype.stringify(mdastRoot) -) +expectType(processorWithRemarkRehype.stringify(hastRoot)) +expectType(processorWithRemarkRehype.stringify(mdastRoot)) expectType(processorWithRemarkRehype.processSync('')) // Mutate plugin (implicit). @@ -431,10 +421,10 @@ expectType(processorWithRemarkRehypeImplicit.parse('')) expectType(processorWithRemarkRehypeImplicit.runSync(mdastRoot)) // @ts-expect-error: not the correct node type. processorWithRemarkRehypeImplicit.runSync(hastRoot) -expectType( +expectType( processorWithRemarkRehypeImplicit.stringify(hastRoot) ) -expectType( +expectType( processorWithRemarkRehypeImplicit.stringify(mdastRoot) ) expectType(processorWithRemarkRehypeImplicit.processSync('')) diff --git a/lib/index.js b/lib/index.js index 71c3cff6..0fcc16a5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -791,9 +791,7 @@ export class Processor extends CallableInstance { const compileResult = self.stringify(compileTree, file) - if (compileResult === null || compileResult === undefined) { - // Empty. - } else if (looksLikeAVFileValue(compileResult)) { + if (looksLikeAVFileValue(compileResult)) { file.value = compileResult } else { file.result = compileResult @@ -1020,7 +1018,7 @@ export class Processor extends CallableInstance { * @param {VFileCompatible | undefined} [file] * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. - * @returns {CompileResult extends undefined ? VFileValue | null : CompileResult} + * @returns {CompileResult extends undefined ? VFileValue : CompileResult} * Textual representation of the tree (see note). * * > 👉 **Note**: unified typically compiles by serializing: most compilers diff --git a/test/process-compilers.js b/test/process-compilers.js index 716fbbdf..4234ac3c 100644 --- a/test/process-compilers.js +++ b/test/process-compilers.js @@ -34,20 +34,6 @@ test('process (compilers)', async function (t) { assert.equal(file.result, undefined) }) - await t.test('should compile `null`', async function () { - const processor = unified() - - processor.Parser = simpleParser - processor.Compiler = function () { - return null - } - - const file = await processor.process('alpha') - - assert.equal(file.value, 'alpha') - assert.equal(file.result, undefined) - }) - await t.test('should compile non-text', async function () { const processor = unified() const result = { From 40f03295dc535624f91a7f086489942aa42e6340 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 13:32:05 +0200 Subject: [PATCH 39/46] Refactor some more code to use JSDoc --- .gitignore | 1 - lib/callable-instance.d.ts | 7 ----- lib/callable-instance.js | 61 ++++++++++++++++++++++---------------- script/fix-types.js | 14 ++++++--- tsconfig.json | 2 +- 5 files changed, 46 insertions(+), 39 deletions(-) delete mode 100644 lib/callable-instance.d.ts diff --git a/.gitignore b/.gitignore index 66103597..fcb26070 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ node_modules/ *.log yarn.lock !/index.d.ts -!/lib/callable-instance.d.ts diff --git a/lib/callable-instance.d.ts b/lib/callable-instance.d.ts deleted file mode 100644 index 0fbc88f7..00000000 --- a/lib/callable-instance.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -type Func = (...argv: Args) => Return - -export type ICallableInstance = new ( - property: string | symbol -) => Func - -export const CallableInstance: ICallableInstance diff --git a/lib/callable-instance.js b/lib/callable-instance.js index e42eb011..17c693de 100644 --- a/lib/callable-instance.js +++ b/lib/callable-instance.js @@ -1,30 +1,39 @@ -/** - * @param {string} property - */ -export function CallableInstance(property) { - /** @type {Function} */ - const self = this - const constr = self.constructor - // Prototypes do exist. - // type-coverage:ignore-next-line - const proto = /** @type {Record} */ (constr.prototype) - const func = proto[property] - const apply = function () { - return func.apply(apply, arguments) - } +export const CallableInstance = + /** + * @type {new , Result>(property: string | symbol) => (...parameters: Parameters) => Result} + */ + ( + /** @type {unknown} */ + ( + /** + * @this {Function} + * @param {string | symbol} property + * @returns {(...parameters: Array) => unknown} + */ + function (property) { + const self = this + const constr = self.constructor + const proto = /** @type {Record} */ ( + // Prototypes do exist. + // type-coverage:ignore-next-line + constr.prototype + ) + const func = proto[property] + /** @type {(...parameters: Array) => unknown} */ + const apply = function () { + return func.apply(apply, arguments) + } - Object.setPrototypeOf(apply, proto) + Object.setPrototypeOf(apply, proto) - const names = Object.getOwnPropertyNames(func) + const names = Object.getOwnPropertyNames(func) - for (const p of names) { - const descriptor = Object.getOwnPropertyDescriptor(func, p) - if (descriptor) Object.defineProperty(apply, p, descriptor) - } + for (const p of names) { + const descriptor = Object.getOwnPropertyDescriptor(func, p) + if (descriptor) Object.defineProperty(apply, p, descriptor) + } - return apply -} - -// Prototypes do exist. -// type-coverage:ignore-next-line -CallableInstance.prototype = Object.create(Function.prototype) + return apply + } + ) + ) diff --git a/script/fix-types.js b/script/fix-types.js index a17b1fcf..7495341f 100644 --- a/script/fix-types.js +++ b/script/fix-types.js @@ -13,19 +13,25 @@ try { const result = file .replace(/declare const Processor_base: [^\n]+/, function () { console.log('Fixed `CallableInstance` import') - return "declare const CallableInstance: import('./callable-instance.js').ICallableInstance" + return "import {CallableInstance} from './callable-instance.js'" }) .replace(/extends Processor_base/, function () { console.log('Fixed `CallableInstance` use') return 'extends CallableInstance<[], Processor>' }) .replace( - /\.\.\.parameters: Parameters_1 \| \[boolean] \| undefined/, - function () { + /\.\.\.parameters: (Parameters_\d) \| \[boolean] \| undefined/, + /** + * + * @param {string} $0 + * @param {string} $1 + * @returns {string} + */ + function ($0, $1) { console.log( 'Fixed `use` overload with plugin, and *non-optional* parameters' ) - return '...parameters: Parameters_1 | [boolean]' + return '...parameters: ' + $1 + ' | [boolean]' } ) diff --git a/tsconfig.json b/tsconfig.json index dd290ca4..ad1496e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "target": "es2020" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "lib/callable-instance.d.ts", "index.d.ts"] + "include": ["**/*.js", "index.d.ts"] } From a44db4603bacb1dad8fbadfa7c5941aa5b62eb66 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 14:00:48 +0200 Subject: [PATCH 40/46] Add `Data`, `Settings` types to augment shared data --- .gitignore | 1 + index.d.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/index.js | 21 +++++++++++-------- test/data.js | 2 ++ test/types.d.ts | 9 ++++++++ tsconfig.json | 2 +- 6 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 test/types.d.ts diff --git a/.gitignore b/.gitignore index fcb26070..69d75b1a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules/ *.log yarn.lock !/index.d.ts +!/test/types.d.ts diff --git a/index.d.ts b/index.d.ts index ffb08843..e2d124a9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,6 +7,7 @@ export type { Compiler, CompilerClass, CompilerFunction, + // `Data` is typed and exposed below. Pluggable, PluggableList, Plugin, @@ -18,6 +19,7 @@ export type { ProcessCallback, Processor, RunCallback, + // `Settings` is typed and exposed below. TransformCallback, Transformer } from './lib/index.js' @@ -40,6 +42,8 @@ export {unified} from './lib/index.js' * ReactNode: ReactNode * } * } + * + * export {} // You may not need this, but it makes sure the file is a module. * ``` * * Use {@link CompileResults `CompileResults`} to access the values. @@ -49,3 +53,54 @@ export interface CompileResultMap { Uint8Array: Uint8Array string: string } + +/** + * Interface of known data that can be supported by all plugins. + * + * Typically, options can be given to a specific plugin, but sometimes it makes + * sense to have information shared with several plugins. + * For example, a list of HTML elements that are self-closing, which is needed + * during all phases. + * + * To type this, do something like: + * + * ```ts + * declare module 'unified' { + * interface Data { + * htmlVoidElements?: Array | undefined + * } + * } + * + * export {} // You may not need this, but it makes sure the file is a module. + * ``` + */ +export interface Data { + settings?: Settings | undefined +} + +/** + * Interface of known extra options, that can be supported by parser and + * compilers. + * + * This exists so that users can use packages such as `remark`, which configure + * both parsers and compilers (in this case `remark-parse` and + * `remark-stringify`), and still provide options for them. + * + * When you make parsers or compilers, that could be packaged up together, + * you should support `this.data('settings')` as input and merge it with + * explicitly passed `options`. + * Then, to type it, using `remark-stringify` as an example, do something like: + * + * ```ts + * declare module 'unified' { + * interface Settings { + * bullet: '*' | '+' | '-' + * // … + * } + * } + * + * export {} // You may not need this, but it makes sure the file is a module. + * ``` + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface Settings {} diff --git a/lib/index.js b/lib/index.js index 0fcc16a5..9dbeec78 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,7 @@ * @typedef {import('vfile').VFileValue} VFileValue * * @typedef {import('../index.js').CompileResultMap} CompileResultMap + * @typedef {import('../index.js').Data} Data */ /** @@ -518,7 +519,7 @@ export class Processor extends CallableInstance { * * @deprecated * This is a private internal property and should not be used. - * @type {{settings?: Record} & Record} + * @type {Data} */ this.namespace = {} @@ -573,26 +574,28 @@ export class Processor extends CallableInstance { * > 👉 **Note**: setting information cannot occur on *frozen* processors. * > Call the processor first to create a new unfrozen processor. * + * @template {keyof Data} Key + * * @overload - * @returns {Record} + * @returns {Data} * * @overload - * @param {Record} dataset + * @param {Data} dataset * @returns {Processor} * * @overload - * @param {string} key - * @returns {unknown} + * @param {Key} key + * @returns {Data[Key]} * * @overload - * @param {string} key - * @param {unknown} value + * @param {Key} key + * @param {Data[Key]} value * @returns {Processor} * - * @param {Record | string} [key] + * @param {Data | Key} [key] * Key to get or set, or entire dataset to set, or nothing to get the * entire dataset (optional). - * @param {unknown} [value] + * @param {Data[Key]} [value] * Value to set (optional). * @returns {unknown} * The processor that `data` is called on when settings, the value at `key` diff --git a/test/data.js b/test/data.js index 671bdaab..6b8df6c2 100644 --- a/test/data.js +++ b/test/data.js @@ -18,6 +18,8 @@ test('`data`', async function (t) { }) await t.test('should not yield data prototypal fields', async function () { + // @ts-expect-error: `toString` is not a typed key of `Data`. + // But it exists on objects, so we test that here. assert.equal(unified().data('toString'), undefined) }) diff --git a/test/types.d.ts b/test/types.d.ts new file mode 100644 index 00000000..5c293242 --- /dev/null +++ b/test/types.d.ts @@ -0,0 +1,9 @@ +declare module 'unified' { + interface Data { + baz?: 'qux' | undefined + foo?: 'bar' | undefined + x?: boolean | undefined + } +} + +export {} // You may not need this, but it makes sure the file is a module. diff --git a/tsconfig.json b/tsconfig.json index ad1496e9..8e70d195 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "target": "es2020" }, "exclude": ["coverage/", "node_modules/"], - "include": ["**/*.js", "index.d.ts"] + "include": ["**/*.js", "test/types.d.ts", "index.d.ts"] } From 714874679636754279a4619030f4a026bf7ca4ea Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 19:02:51 +0200 Subject: [PATCH 41/46] Refactor docs --- index.d.ts | 10 +- index.test-d.ts | 4 + lib/index.js | 279 +++++----- readme.md | 1382 +++++++++++++++++++++++++++++------------------ test/types.d.ts | 5 +- 5 files changed, 1025 insertions(+), 655 deletions(-) diff --git a/index.d.ts b/index.d.ts index e2d124a9..477f04f6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -import type {VFileValue} from 'vfile' +import type {Value} from 'vfile' import type {CompileResults} from './lib/index.js' export type { @@ -8,13 +8,13 @@ export type { CompilerClass, CompilerFunction, // `Data` is typed and exposed below. + Parser, + ParserClass, + ParserFunction, Pluggable, PluggableList, Plugin, PluginTuple, - Parser, - ParserClass, - ParserFunction, Preset, ProcessCallback, Processor, @@ -29,7 +29,7 @@ export {unified} from './lib/index.js' /** * Interface of known results from compilers. * - * Normally, compilers result in text ({@link VFileValue `VFileValue`}). + * Normally, compilers result in text ({@link Value `Value`} of `vfile`). * When you compile to something else, such as a React node (as in, * `rehype-react`), you can augment this interface to include that type. * diff --git a/index.test-d.ts b/index.test-d.ts index 8a3f45f7..05a5a226 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -477,6 +477,10 @@ declare module './index.js' { interface CompileResultMap { ReactNode: ReactNode } + + interface Data { + something?: string | undefined + } } // Compile plugin (to a non-node). diff --git a/lib/index.js b/lib/index.js index 9dbeec78..f0072c5f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,8 +3,8 @@ * * @typedef {import('unist').Node} Node * - * @typedef {import('vfile').VFileCompatible} VFileCompatible - * @typedef {import('vfile').VFileValue} VFileValue + * @typedef {import('vfile').Compatible} Compatible + * @typedef {import('vfile').Value} Value * * @typedef {import('../index.js').CompileResultMap} CompileResultMap * @typedef {import('../index.js').Data} Data @@ -31,12 +31,12 @@ * and {@link VFile `VFile`} representation of the document to compile. * * `Compiler` can be a normal function, in which case it should return the - * textual representation of the given tree (`string`). + * textual representation of the given tree (typically `string`). * * `Compiler` can also be a constructor function (a function with a `compile` * field in its `prototype`), in which case it is constructed with `new`. * Instances must have a `compile` method that is called without arguments - * and should return a `string`. + * and typically returns a `string`. * * > 👉 **Note**: unified typically compiles by serializing: most compilers * > return `string` (or `Uint8Array`). @@ -159,38 +159,35 @@ * * If the plugin sets a {@link Compiler `Compiler`}, this should be * result it yields. * @typedef {( - * (this: Processor, ...parameters: PluginParameters) => - * Input extends string ? // Parser. - * Output extends Node | undefined ? undefined | void : never : - * Output extends CompileResults ? // Compiler. - * Input extends Node | undefined ? undefined | void : never : - * Transformer< - * Input extends Node ? Input : Node, - * Output extends Node ? Output : Node - * > | undefined | void + * (this: Processor, ...parameters: PluginParameters) => + * Input extends string ? // Parser. + * Output extends Node | undefined ? undefined | void : never : + * Output extends CompileResults ? // Compiler. + * Input extends Node | undefined ? undefined | void : never : + * Transformer< + * Input extends Node ? Input : Node, + * Output extends Node ? Output : Node + * > | undefined | void * )} Plugin - * **Plugins** configure the processors they are applied on in the following + * Single plugin. + * + * Plugins configure the processors they are applied on in the following * ways: * * * they change the processor, such as the parser, the compiler, or by * configuring data * * they specify how to handle trees and files * - * Plugins are a concept. - * They materialize as `Attacher`s. - * - * Attachers are materialized plugins. - * They are functions that can receive options and configure the processor. - * - * Attachers change the processor, such as the parser, the compiler, by - * configuring data, or by specifying how the tree and file are handled. + * In practise, they are functions that can receive options and configure the + * processor (`this`). * - * > 👉 **Note**: attachers are called when the processor is *frozen*, - * > not when they are applied. + * > 👉 **Note**: plugins are called when the processor is *frozen*, not when + * > they are applied. */ /** - * Tuple of a plugin and its setting(s). + * Tuple of a plugin and its configuration. + * * The first item is a plugin, the rest are its parameters. * * @template {Array} [TupleParameters=[]] @@ -205,12 +202,6 @@ * * If the plugin sets a {@link Compiler `Compiler`}, this should be the * node it expects. * @template [Output=undefined] (optional). - * @typedef {( - * [ - * plugin: Plugin, - * ...parameters: TupleParameters - * ] - * )} PluginTuple * Value that is yielded as output. * * * If the plugin returns a {@link Transformer `Transformer`}, this @@ -219,16 +210,22 @@ * node that it yields. * * If the plugin sets a {@link Compiler `Compiler`}, this should be * result it yields. + * @typedef {( + * [ + * plugin: Plugin, + * ...parameters: TupleParameters + * ] + * )} PluginTuple */ /** * @typedef Preset - * Presets are sharable configuration. + * Sharable configuration. * * They can contain plugins and settings. * @property {PluggableList | undefined} [plugins] * List of plugins and presets (optional). - * @property {Record | undefined} [settings] + * @property {Data | undefined} [settings] * Shared settings for parsers and compilers (optional). */ @@ -264,6 +261,24 @@ * Nothing. */ +/** + * @template {Node} [Output=Node] + * Node type that the transformer yields (default: `Node`). + * @callback TransformCallback + * Callback passed to transforms. + * + * If the signature of a `transformer` accepts a third argument, the + * transformer may perform asynchronous operations, and must call it. + * @param {Error | undefined} [error] + * Fatal error to stop the process (optional). + * @param {Output | undefined} [tree] + * New, changed, tree (optional). + * @param {VFile | undefined} [file] + * New, changed, file (optional). + * @returns {undefined} + * Nothing. + */ + /** * @template {Node} [Input=Node] * Node type that the transformer expects (default: `Node`). @@ -280,6 +295,10 @@ * The run phase is handled by [`trough`][trough], see its documentation for * the exact semantics of these functions. * + * > 👉 **Note**: you should likely ignore `next`: don’t accept it. + * > it supports callback-style async work. + * > But promises are likely easier to reason about. + * * [trough]: https://github.com/wooorm/trough#function-fninput-next * @param {Input} tree * Tree to handle. @@ -288,12 +307,12 @@ * @param {TransformCallback} next * Callback. * @returns {( - * Promise | - * Promise | // For some reason this is needed separately. - * Output | - * Error | - * undefined | - * void + * Promise | + * Promise | // For some reason this is needed separately. + * Output | + * Error | + * undefined | + * void * )} * If you accept `next`, nothing. * Otherwise: @@ -304,22 +323,6 @@ * * `Promise` or `Node` — new, changed, tree */ -/** - * @template {Node} [Output=Node] - * Node type that the transformer yields (default: `Node`). - * @callback TransformCallback - * If the signature of a `transformer` accepts a third argument, the - * transformer may perform asynchronous operations, and must call `next()`. - * @param {Error | undefined} [error] - * Fatal error to stop the process (optional). - * @param {Output | undefined} [tree] - * New, changed, tree (optional). - * @param {VFile | undefined} [file] - * New, changed, file (optional). - * @returns {undefined} - * Nothing. - */ - /** * @template {Node | undefined} ParseTree * Output of `parse`. @@ -382,7 +385,7 @@ * @template {CompileResults | undefined} Result * Node type that the transformer yields. * @typedef {( - * Result extends VFileValue | undefined ? + * Result extends Value | undefined ? * VFile : * VFile & {result: Result} * )} VFileWithOutput @@ -400,6 +403,14 @@ import {trough} from 'trough' import {VFile} from 'vfile' import {CallableInstance} from './callable-instance.js' +// To do: we could drop class support for the parser and compiler, +// we don’t use that anymore? +// Would be less breaking if we do `processor.compiler || processor.Compiler`. + +// To do: we could start yielding `never` in TS when a parser is missing and +// `parse` is called. +// Currently, we allow directly setting `processor.Parser`, which is untyped. + const own = {}.hasOwnProperty /** @@ -424,33 +435,7 @@ export class Processor extends CallableInstance { super('copy') /** - * A **compiler** handles the compiling of a syntax tree to something else - * (in most cases, text). - * - * It is used in the stringify phase and called with a {@link Node `Node`} - * and {@link VFile `VFile`} representation of the document to compile. - * - * `Compiler` can be a normal function, in which case it should return the - * textual representation of the given tree (`string`). - * - * `Compiler` can also be a constructor function (a function with a - * `compile` field in its `prototype`), in which case it is constructed with - * `new`. - * Instances must have a `compile` method that is called without arguments - * and should return a `string`. - * - * > 👉 **Note**: unified typically compiles by serializing: most compilers - * > return `string` (or `Uint8Array`). - * > Some compilers, such as the one configured with - * > [`rehype-react`][rehype-react], return other values (in this case, a - * > React tree). - * > If you’re using a compiler that doesn’t serialize, expect different - * > result values. - * > - * > To register custom results in TypeScript, add them to - * > {@link CompileResultMap `CompileResultMap`}. - * - * [rehype-react]: https://github.com/rehypejs/rehype-react + * Compiler to use. * * @type {( * Compiler< @@ -463,18 +448,7 @@ export class Processor extends CallableInstance { this.Compiler = undefined /** - * A **parser** handles the parsing of text to a syntax tree. - * - * It is used in the parse phase and is called with a `string` and - * {@link VFile `VFile`} of the document to parse. - * - * `Parser` can be a normal function, in which case it must return the - * syntax tree representation of the given file ({@link Node `Node`}). - * - * `Parser` can also be a constructor function (a function with a `parse` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `parse` method that is called without arguments - * and must return a {@link Node `Node`}. + * Parser to use. * * @type {( * Parser | @@ -574,6 +548,26 @@ export class Processor extends CallableInstance { * > 👉 **Note**: setting information cannot occur on *frozen* processors. * > Call the processor first to create a new unfrozen processor. * + * > 👉 **Note**: to register custom data in TypeScript, augment the + * > {@link Data `Data`} interface. + * + * @example + * This example show how to get and set info: + * + * ```js + * import {unified} from 'unified' + * + * const processor = unified().data('alpha', 'bravo') + * + * processor.data('alpha') // => 'bravo' + * + * processor.data() // => {alpha: 'bravo'} + * + * processor.data({charlie: 'delta'}) + * + * processor.data() // => {charlie: 'delta'} + * ``` + * * @template {keyof Data} Key * * @overload @@ -598,8 +592,8 @@ export class Processor extends CallableInstance { * @param {Data[Key]} [value] * Value to set (optional). * @returns {unknown} - * The processor that `data` is called on when settings, the value at `key` - * when getting, or the entire dataset when getting w/o key. + * The current processor when setting, the value at `key` when getting, or + * the entire dataset when getting without key. */ data(key, value) { if (typeof key === 'string') { @@ -640,7 +634,7 @@ export class Processor extends CallableInstance { * `.stringify()`, `.process()`, or `.processSync()` are called. * * @returns {Processor} - * The processor that `freeze` was called on. + * The current processor. */ freeze() { if (this.frozen) { @@ -684,9 +678,9 @@ export class Processor extends CallableInstance { * > 👉 **Note**: `parse` performs the parse phase, not the run phase or other * > phases. * - * @param {VFileCompatible | undefined} [file] - * file to parse (optional); typically `string`; any value accepted as `x` - * in `new VFile(x)`. + * @param {Compatible | undefined} [file] + * file to parse (optional); typically `string` or `VFile`; any value + * accepted as `x` in `new VFile(x)`. * @returns {ParseTree extends undefined ? Node : ParseTree} * Syntax tree representing `file`. */ @@ -723,21 +717,22 @@ export class Processor extends CallableInstance { * > 👉 **Note**: `process` performs the parse, run, and stringify phases. * * @overload - * @param {VFileCompatible | undefined} file + * @param {Compatible | undefined} file * @param {ProcessCallback>} done * @returns {undefined} * * @overload - * @param {VFileCompatible | undefined} [file] + * @param {Compatible | undefined} [file] * @returns {Promise>} * - * @param {VFileCompatible | undefined} [file] - * File (optional); any value accepted as `x` in `new VFile(x)`. + * @param {Compatible | undefined} [file] + * File (optional); typically `string` or `VFile`]; any value accepted as + * `x` in `new VFile(x)`. * @param {ProcessCallback> | undefined} [done] * Callback (optional). * @returns {Promise | undefined} * Nothing if `done` is given. - * Otherwise `Promise`, rejected with a fatal error or resolved with the + * Otherwise a promise, rejected with a fatal error or resolved with the * processed file. * * The parsed, transformed, and compiled value is available at @@ -794,7 +789,7 @@ export class Processor extends CallableInstance { const compileResult = self.stringify(compileTree, file) - if (looksLikeAVFileValue(compileResult)) { + if (looksLikeAValue(compileResult)) { file.value = compileResult } else { file.result = compileResult @@ -830,8 +825,9 @@ export class Processor extends CallableInstance { * * > 👉 **Note**: `processSync` performs the parse, run, and stringify phases. * - * @param {VFileCompatible | undefined} [file] - * File (optional); any value accepted as `x` in `new VFile(x)`. + * @param {Compatible | undefined} [file] + * File (optional); typically `string` or `VFile`; any value accepted as + * `x` in `new VFile(x)`. * @returns {VFileWithOutput} * The processed file. * @@ -891,20 +887,20 @@ export class Processor extends CallableInstance { * * @overload * @param {HeadTree extends undefined ? Node : HeadTree} tree - * @param {VFileCompatible | undefined} file + * @param {Compatible | undefined} file * @param {RunCallback} done * @returns {undefined} * * @overload * @param {HeadTree extends undefined ? Node : HeadTree} tree - * @param {VFileCompatible | undefined} [file] + * @param {Compatible | undefined} [file] * @returns {Promise} * * @param {HeadTree extends undefined ? Node : HeadTree} tree * Tree to transform and inspect. * @param {( * RunCallback | - * VFileCompatible + * Compatible * )} [file] * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. @@ -912,7 +908,7 @@ export class Processor extends CallableInstance { * Callback (optional). * @returns {Promise | undefined} * Nothing if `done` is given. - * Otherwise, `Promise` rejected with a fatal error or resolved with the + * Otherwise, a promise rejected with a fatal error or resolved with the * transformed tree. */ run(tree, file, done) { @@ -980,7 +976,7 @@ export class Processor extends CallableInstance { * * @param {HeadTree extends undefined ? Node : HeadTree} tree * Tree to transform and inspect. - * @param {VFileCompatible | undefined} [file] + * @param {Compatible | undefined} [file] * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. * @returns {TailTree extends undefined ? Node : TailTree} @@ -1014,14 +1010,14 @@ export class Processor extends CallableInstance { * > 👉 **Note**: `stringify` freezes the processor if not already *frozen*. * * > 👉 **Note**: `stringify` performs the stringify phase, not the run phase - * or other phases. + * > or other phases. * * @param {CompileTree extends undefined ? Node : CompileTree} tree - * Tree to compile - * @param {VFileCompatible | undefined} [file] + * Tree to compile. + * @param {Compatible | undefined} [file] * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. - * @returns {CompileResult extends undefined ? VFileValue : CompileResult} + * @returns {CompileResult extends undefined ? Value : CompileResult} * Textual representation of the tree (see note). * * > 👉 **Note**: unified typically compiles by serializing: most compilers @@ -1072,14 +1068,20 @@ export class Processor extends CallableInstance { } /** - * Configure the processor to use a plugin, a list of usable - * values, or with a preset. + * Configure the processor to use a plugin, a list of usable values, or a + * preset. * * If the processor is already using a plugin, the previous plugin * configuration is changed based on the options that are passed in. * In other words, the plugin is not added a second time. * + * > 👉 **Note**: `use` cannot be called on *frozen* processors. + * > Call the processor first to create a new unfrozen processor. + * * @example + * There are many ways to pass plugins to `.use()`. + * This example gives an overview: + * * ```js * import {unified} from 'unified' * @@ -1238,8 +1240,33 @@ export class Processor extends CallableInstance { } } +// Note: this returns a *callable* instance. +// That’s why it’s documented as a function. /** - * Base processor. + * Create a new processor. + * + * @example + * This example shows how a new processor can be created (from `remark`) and linked + * to **stdin**(4) and **stdout**(4). + * + * ```js + * import process from 'node:process' + * import concatStream from 'concat-stream' + * import {remark} from 'remark' + * + * process.stdin.pipe( + * concatStream(function (buf) { + * process.stdout.write(String(remark().processSync(buf))) + * }) + * ) + * ``` + * + * @returns + * New *unfrozen* processor (`processor`). + * + * This processor is configured to work the same as its ancestor. + * When the descendant processor is configured in the future it does not + * affect the ancestral processor. */ export const unified = new Processor().freeze() @@ -1300,7 +1327,7 @@ function assertParser(name, value) { * * @param {string} name * @param {unknown} value - * @returns {asserts value is Compiler} + * @returns {asserts value is Compiler} */ function assertCompiler(name, value) { if (typeof value !== 'function') { @@ -1357,7 +1384,7 @@ function assertDone(name, asyncName, complete) { } /** - * @param {VFileCompatible | undefined} [value] + * @param {Compatible | undefined} [value] * @returns {VFile} */ function vfile(value) { @@ -1365,7 +1392,7 @@ function vfile(value) { } /** - * @param {VFileCompatible | undefined} [value] + * @param {Compatible | undefined} [value] * @returns {value is VFile} */ function looksLikeAVFile(value) { @@ -1379,9 +1406,9 @@ function looksLikeAVFile(value) { /** * @param {unknown} [value] - * @returns {value is VFileValue} + * @returns {value is Value} */ -function looksLikeAVFileValue(value) { +function looksLikeAValue(value) { return typeof value === 'string' || isUint8Array(value) } diff --git a/readme.md b/readme.md index ce24b8ae..d727415f 100644 --- a/readme.md +++ b/readme.md @@ -19,19 +19,37 @@ * [Overview](#overview) * [API](#api) * [`processor()`](#processor) - * [`processor.use(plugin[, options])`](#processoruseplugin-options) + * [`processor.Compiler`](#processorcompiler) + * [`processor.Parser`](#processorparser) + * [`processor.data([key[, value]])`](#processordatakey-value) + * [`processor.freeze()`](#processorfreeze) * [`processor.parse(file)`](#processorparsefile) - * [`processor.stringify(tree[, file])`](#processorstringifytree-file) - * [`processor.run(tree[, file][, done])`](#processorruntree-file-done) - * [`processor.runSync(tree[, file])`](#processorrunsynctree-file) * [`processor.process(file[, done])`](#processorprocessfile-done) * [`processor.processSync(file)`](#processorprocesssyncfile) - * [`processor.data([key[, value]])`](#processordatakey-value) - * [`processor.freeze()`](#processorfreeze) -* [`Plugin`](#plugin) - * [`function attacher(options?)`](#function-attacheroptions) - * [`function transformer(tree, file[, next])`](#function-transformertree-file-next) -* [`Preset`](#preset) + * [`processor.run(tree[, file][, done])`](#processorruntree-file-done) + * [`processor.runSync(tree[, file])`](#processorrunsynctree-file) + * [`processor.stringify(tree[, file])`](#processorstringifytree-file) + * [`processor.use(plugin[, options])`](#processoruseplugin-options) + * [`CompileResultMap`](#compileresultmap) + * [`CompileResults`](#compileresults) + * [`Compiler`](#compiler) + * [`CompilerClass`](#compilerclass) + * [`CompilerFunction`](#compilerfunction) + * [`Data`](#data) + * [`Parser`](#parser) + * [`ParserClass`](#parserclass) + * [`ParserFunction`](#parserfunction) + * [`Pluggable`](#pluggable) + * [`PluggableList`](#pluggablelist) + * [`Plugin`](#plugin) + * [`PluginTuple`](#plugintuple) + * [`Preset`](#preset) + * [`ProcessCallback`](#processcallback) + * [`Processor`](#processor-1) + * [`RunCallback`](#runcallback) + * [`Settings`](#settings) + * [`TransformCallback`](#transformcallback) + * [`Transformer`](#transformer) * [Types](#types) * [Compatibility](#compatibility) * [Contribute](#contribute) @@ -45,7 +63,7 @@ unified is two things: * **unified** is a collective of 500+ free and open source packages that work with content as structured data (ASTs) -* `unified` (this project) is the core package, used in 800k+ projects on GH, +* `unified` (this project) is the core package, used in 1.3m+ projects on GH, to process content with plugins Several ecosystems are built on unified around different kinds of content. @@ -72,8 +90,8 @@ generator. You can connect utilities together and make your own plugins that check for problems and transform from one thing to another. -When you are dealing with one type of content (such as markdown), it’s -recommended to use the main package of that ecosystem instead (so `remark`). +When you are dealing with one type of content (such as markdown), you can use +the main package of that ecosystem instead (so `remark`). When you are dealing with different kinds of content (such as markdown and HTML), it’s recommended to use `unified` itself, and pick and choose the plugins you need. @@ -81,7 +99,7 @@ you need. ## Install This package is [ESM only][esm]. -In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: +In Node.js (version 16+), install with [npm][]: ```sh npm install unified @@ -104,12 +122,12 @@ In browsers with [`esm.sh`][esmsh]: ## Use ```js -import {unified} from 'unified' -import remarkParse from 'remark-parse' -import remarkRehype from 'remark-rehype' import rehypeDocument from 'rehype-document' import rehypeFormat from 'rehype-format' import rehypeStringify from 'rehype-stringify' +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import {unified} from 'unified' import {reporter} from 'vfile-reporter' const file = await unified() @@ -152,11 +170,11 @@ no issues found `unified` is an interface for processing content with syntax trees. Syntax trees are a representation of content understandable to programs. -Those programs, called *[plugins][plugin]*, take these trees and inspect and +Those programs, called *[plugins][api-plugin]*, take these trees and inspect and modify them. -To get to the syntax tree from text, there is a *[parser][]*. -To get from that back to text, there is a *[compiler][]*. -This is the *[process][]* of a *processor*. +To get to the syntax tree from text, there is a *[parser][api-parser]*. +To get from that back to text, there is a *[compiler][api-compiler]*. +This is the *[process][api-process]* of a *processor*. ```ascii | ........................ process ........................... | @@ -205,8 +223,8 @@ the ancestral processor. When processors are exposed from a module (for example, `unified` itself) they should not be configured directly, as that would change their behavior for all module users. -Those processors are *[frozen][freeze]* and they should be called to create a -new processor before they are used. +Those processors are *[frozen][api-freeze]* and they should be called to create +a new processor before they are used. ###### File @@ -279,8 +297,8 @@ There are also a few plugins that work in any ecosystem: ###### Configuration -Processors are configured with [plugins][plugin] or with the [`data`][data] -method. +Processors are configured with [plugins][api-plugin] or with the +[`data`][api-data] method. Most plugins also accept configuration through options. See each plugin’s readme for more info. @@ -299,14 +317,14 @@ The [API][] provided by `unified` allows multiple files to be processed and gives access to metadata (such as lint messages): ```js -import {unified} from 'unified' +import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkPresetLintMarkdownStyleGuide from 'remark-preset-lint-markdown-style-guide' +import remarkRehype from 'remark-rehype' import remarkRetext from 'remark-retext' import retextEnglish from 'retext-english' import retextEquality from 'retext-equality' -import remarkRehype from 'remark-rehype' -import rehypeStringify from 'rehype-stringify' +import {unified} from 'unified' import {reporter} from 'vfile-reporter' const file = await unified() @@ -324,8 +342,8 @@ console.log(String(file)) Yields: ```txt - 1:16-1:24 warning Emphasis should use `*` as a marker emphasis-marker remark-lint - 1:30-1:34 warning `guys` may be insensitive, use `people`, `persons`, `folks` instead gals-man retext-equality +1:16-1:24 warning Emphasis should use `*` as a marker emphasis-marker remark-lint +1:30-1:34 warning `guys` may be insensitive, use `people`, `persons`, `folks` instead gals-man retext-equality ⚠ 2 warnings ``` @@ -369,14 +387,15 @@ There is no default export. ### `processor()` -Create a processor. +Create a new processor. ###### Returns -New *[unfrozen][freeze]* processor (`processor`) that is configured to work the -same as its ancestor. -When the descendant processor is configured in the future it does not affect the -ancestral processor. +New *[unfrozen][api-freeze]* processor ([`processor`][api-processor]). + +This processor is configured to work the same as its ancestor. +When the descendant processor is configured in the future it does not affect +the ancestral processor. ###### Example @@ -395,73 +414,152 @@ process.stdin.pipe( ) ``` -### `processor.use(plugin[, options])` +### `processor.Compiler` -Configure the processor to use a plugin and optionally configure that plugin -with options. +Compiler to use ([`Compiler`][api-compiler], optional). -If the processor is already using a plugin, the previous plugin configuration -is changed based on the options that are passed in. -In other words, the plugin is not added a second time. +### `processor.Parser` + +Parser to use ([`Parser`][api-parser], optional). + +### `processor.data([key[, value]])` + +Configure the processor with info available to all plugins. +Information is stored in an object. + +Typically, options can be given to a specific plugin, but sometimes it makes +sense to have information shared with several plugins. +For example, a list of HTML elements that are self-closing, which is needed +during all [phases][overview]. -> 👉 **Note**: `use` cannot be called on *[frozen][freeze]* processors. +> 👉 **Note**: setting information cannot occur on *[frozen][api-freeze]* +> processors. > Call the processor first to create a new unfrozen processor. +> 👉 **Note**: to register custom data in TypeScript, augment the +> [`Data`][api-data] interface. + ###### Signatures -* `processor.use(plugin[, options])` -* `processor.use(preset)` -* `processor.use(list)` +* `processor = processor.data(key, value)` +* `processor = processor.data(dataset)` +* `value = processor.data(key)` +* `dataset = processor.data()` ###### Parameters -* `plugin` ([`Attacher`][plugin]) -* `options` (`*`, optional) — configuration for `plugin` -* `preset` (`Object`) — object with an optional `plugins` (set to `list`), - and/or an optional `settings` object -* `list` (`Array`) — list of plugins, presets, and pairs (`plugin` and - `options` in an array) +* `key` ([`keyof Data`][api-data], optional) — field to get +* `value` ([`Data[key]`][api-data]) — value to set +* `values` ([`Data`][api-data]) — values to set ###### Returns -The processor that `use` was called on (`processor`). +The current processor when setting ([`processor`][api-processor]), the value at +`key` when getting ([`Data[key]`][api-data]), or the entire dataset when +getting without key ([`Data`][api-data]). ###### Example -There are many ways to pass plugins to `.use()`. -This example gives an overview: +This example show how to get and set info: ```js import {unified} from 'unified' -unified() - // Plugin with options: - .use(pluginA, {x: true, y: true}) - // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): - .use(pluginA, {y: false, z: true}) - // Plugins: - .use([pluginB, pluginC]) - // Two plugins, the second with options: - .use([pluginD, [pluginE, {}]]) - // Preset with plugins and settings: - .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}}) - // Settings only: - .use({settings: {position: false}}) +const processor = unified().data('alpha', 'bravo') + +processor.data('alpha') // => 'bravo' + +processor.data() // => {alpha: 'bravo'} + +processor.data({charlie: 'delta'}) + +processor.data() // => {charlie: 'delta'} +``` + +### `processor.freeze()` + +Freeze a processor. + +Frozen processors are meant to be extended and not to be configured directly. + +When a processor is frozen it cannot be unfrozen. +New processors working the same way can be created by calling the processor. + +It’s possible to freeze processors explicitly by calling `.freeze()`. +Processors freeze automatically when `.parse()`, `.run()`, `.runSync()`, +`.stringify()`, `.process()`, or `.processSync()` are called. + +###### Returns + +The current processor ([`processor`][api-processor]). + +###### Example + +This example, `index.js`, shows how `rehype` prevents extensions to itself: + +```js +import rehypeParse from 'rehype-parse' +import rehypeStringify from 'rehype-stringify' +import {unified} from 'unified' + +export const rehype = unified().use(rehypeParse).use(rehypeStringify).freeze() +``` + +That processor can be used and configured like so: + +```js +import {rehype} from 'rehype' +import rehypeFormat from 'rehype-format' +// … + +rehype() + .use(rehypeFormat) + // … +``` + +A similar looking example is broken as operates on the frozen interface. +If this behavior was allowed it would result in unexpected behavior so an error +is thrown. +**This is not valid**: + +```js +import {rehype} from 'rehype' +import rehypeFormat from 'rehype-format' +// … + +rehype + .use(rehypeFormat) + // … +``` + +Yields: + +```txt +~/node_modules/unified/index.js:426 + throw new Error( + ^ + +Error: Cannot call `use` on a frozen processor. +Create a new processor first, by calling it: use `processor()` instead of `processor`. + at assertUnfrozen (~/node_modules/unified/index.js:426:11) + at Function.use (~/node_modules/unified/index.js:165:5) + … ``` ### `processor.parse(file)` Parse text to a syntax tree. -> 👉 **Note**: `parse` freezes the processor if not already *[frozen][freeze]*. +> 👉 **Note**: `parse` freezes the processor if not already +> *[frozen][api-freeze]*. > 👉 **Note**: `parse` performs the [parse phase][overview], not the run phase > or other phases. ###### Parameters -* `file` ([`VFile`][vfile]) — file to parse; typically `string`; any value - accepted as `x` in `new VFile(x)` +* `file` ([`Compatible`][vfile-compatible]) — file to parse; typically + `string` or [`VFile`][vfile]; any value accepted as `x` in `new VFile(x)` ###### Returns @@ -472,8 +570,8 @@ Syntax tree representing `file` ([`Node`][node]). This example shows how `parse` can be used to create a tree from a file. ```js -import {unified} from 'unified' import remarkParse from 'remark-parse' +import {unified} from 'unified' const tree = unified().use(remarkParse).parse('# Hello world!') @@ -486,7 +584,7 @@ Yields: { type: 'root', children: [ - {type: 'heading', depth: 1, children: [Array], position: [Position]} + {type: 'heading', depth: 1, children: [Array], position: [Object]} ], position: { start: {line: 1, column: 1, offset: 0}, @@ -495,125 +593,195 @@ Yields: } ``` -#### `processor.Parser` - -A **parser** handles the parsing of text to a syntax tree. - -It is used in the [parse phase][overview] and is called with a `string` and -[`VFile`][vfile] of the document to parse. - -`Parser` can be a normal function, in which case it must return the syntax -tree representation of the given file ([`Node`][node]). +### `processor.process(file[, done])` -`Parser` can also be a constructor function (a function with a `parse` field in -its `prototype`), in which case it is constructed with `new`. -Instances must have a `parse` method that is called without arguments and must -return a [`Node`][node]. +Process the given file as configured on the processor. -### `processor.stringify(tree[, file])` +> 👉 **Note**: `process` freezes the processor if not already +> *[frozen][api-freeze]*. -Compile a syntax tree. +> 👉 **Note**: `process` performs the [parse, run, and stringify +> phases][overview]. -> 👉 **Note**: `stringify` freezes the processor if not already -> *[frozen][freeze]*. +###### Signatures -> 👉 **Note**: `stringify` performs the [stringify phase][overview], not the run -> phase or other phases. +* `processor.process(file, done)` +* `Promise = processor.process(file?)` ###### Parameters -* `tree` ([`Node`][node]) — tree to compile -* `file` ([`VFile`][vfile], optional) — file associated with `node`; any - value accepted as `x` in `new VFile(x)` +* `file` ([`Compatible`][vfile-compatible], optional) — file; typically + `string` or [`VFile`][vfile]; any value accepted as `x` in `new VFile(x)` +* `done` ([`ProcessCallback`][api-process-callback], optional) — callback ###### Returns -Textual representation of the tree (`string` or `Uint8Array`, see note). +Nothing if `done` is given (`undefined`). +Otherwise a promise, rejected with a fatal error or resolved with the +processed file ([`Promise`][vfile]). + +The parsed, transformed, and compiled value is available at `file.value` (see +note). > 👉 **Note**: unified typically compiles by serializing: most -> [compilers][compiler] return `string` (or `Uint8Array`). +> compilers return `string` (or `Uint8Array`). > Some compilers, such as the one configured with > [`rehype-react`][rehype-react], return other values (in this case, a React > tree). > If you’re using a compiler that doesn’t serialize, expect different result > values. +> +> To register custom results in TypeScript, add them to +> [`CompileResultMap`][api-compile-result-map]. ###### Example -This example shows how `stringify` can be used to serialize a syntax tree: +This example shows how `process` can be used to process a file: ```js -import {unified} from 'unified' +import rehypeDocument from 'rehype-document' +import rehypeFormat from 'rehype-format' import rehypeStringify from 'rehype-stringify' -import {h} from 'hastscript' - -const tree = h('h1', 'Hello world!') +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import {unified} from 'unified' -const doc = unified().use(rehypeStringify).stringify(tree) +const file = await unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeDocument, {title: '👋🌍'}) + .use(rehypeFormat) + .use(rehypeStringify) + .process('# Hello world!') -console.log(doc) +console.log(String(file)) ``` Yields: ```html -

Hello world!

+ + + + + 👋🌍 + + + +

Hello world!

+ + ``` -#### `processor.Compiler` +### `processor.processSync(file)` -A **compiler** handles the compiling of a syntax tree to something else (in -most cases, text). +Process the given file as configured on the processor. -It is used in the [stringify phase][overview] and called with a [`Node`][node] -and [`VFile`][file] representation of the document to compile. +An error is thrown if asynchronous transforms are configured. -`Compiler` can be a normal function, in which case it should return the textual -representation of the given tree (`string`). +> 👉 **Note**: `processSync` freezes the processor if not already +> *[frozen][api-freeze]*. -`Compiler` can also be a constructor function (a function with a `compile` -field in its `prototype`), in which case it is constructed -with `new`. -Instances must have a `compile` method that is called without arguments and -should return a `string`. +> 👉 **Note**: `processSync` performs the [parse, run, and stringify +> phases][overview]. -> 👉 **Note**: unified typically compiles by serializing: most compilers -> return `string` (or `Uint8Array`). +###### Parameters + +* `file` ([`Compatible`][vfile-compatible], optional) — file; typically + `string` or [`VFile`][vfile]; any value accepted as `x` in `new VFile(x)` + +###### Returns + +The processed file ([`VFile`][vfile]). + +The parsed, transformed, and compiled value is available at `file.value` (see +note). + +> 👉 **Note**: unified typically compiles by serializing: most +> compilers return `string` (or `Uint8Array`). > Some compilers, such as the one configured with > [`rehype-react`][rehype-react], return other values (in this case, a React > tree). > If you’re using a compiler that doesn’t serialize, expect different result > values. +> +> To register custom results in TypeScript, add them to +> [`CompileResultMap`][api-compile-result-map]. + +###### Example + +This example shows how `processSync` can be used to process a file, if all +transformers are synchronous. + +```js +import rehypeDocument from 'rehype-document' +import rehypeFormat from 'rehype-format' +import rehypeStringify from 'rehype-stringify' +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import {unified} from 'unified' + +const processor = unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeDocument, {title: '👋🌍'}) + .use(rehypeFormat) + .use(rehypeStringify) + +console.log(String(processor.processSync('# Hello world!'))) +``` + +Yields: + +```html + + + + + 👋🌍 + + + +

Hello world!

+ + +``` ### `processor.run(tree[, file][, done])` -Run *[transformers][transformer]* on a syntax tree. +Run *[transformers][api-transformer]* on a syntax tree. -> 👉 **Note**: `run` freezes the processor if not already *[frozen][freeze]*. +> 👉 **Note**: `run` freezes the processor if not already +> *[frozen][api-freeze]*. > 👉 **Note**: `run` performs the [run phase][overview], not other phases. +###### Signatures + +* `processor.run(tree, done)` +* `processor.run(tree, file, done)` +* `Promise = processor.run(tree, file?)` + ###### Parameters * `tree` ([`Node`][node]) — tree to transform and inspect -* `file` ([`VFile`][vfile], optional) — any value accepted as `x` in - `new VFile(x)` -* `done` ([`Function`][run-done], optional) — callback +* `file` ([`Compatible`][vfile-compatible], optional) — file associated + with `node`; any value accepted as `x` in `new VFile(x)` +* `done` ([`RunCallback`][api-run-callback], optional) — callback ###### Returns Nothing if `done` is given (`undefined`). -A [`Promise`][promise] otherwise. -The promise is rejected with a fatal error or resolved with the transformed -tree ([`Node`][node]). +Otherwise, a promise rejected with a fatal error or resolved with the +transformed tree ([`Promise`][node]). ###### Example This example shows how `run` can be used to transform a tree: ```js -import {unified} from 'unified' import remarkReferenceLinks from 'remark-reference-links' +import {unified} from 'unified' import {u} from 'unist-builder' const tree = u('root', [ @@ -634,361 +802,405 @@ Yields: type: 'root', children: [ {type: 'paragraph', children: [Array]}, - {type: 'definition', identifier: '1', title: undefined, url: undefined} + {type: 'definition', identifier: '1', title: '', url: undefined} ] } ``` -#### `function done(err[, tree, file])` +### `processor.runSync(tree[, file])` -Callback called when transformers are done. +Run *[transformers][api-transformer]* on a syntax tree. -Called with either an error or results. - -###### Parameters - -* `err` (`Error`, optional) — fatal error -* `tree` ([`Node`][node], optional) — transformed tree -* `file` ([`VFile`][vfile], optional) — file - -### `processor.runSync(tree[, file])` - -Run *[transformers][transformer]* on a syntax tree. An error is thrown if asynchronous transforms are configured. > 👉 **Note**: `runSync` freezes the processor if not already -> *[frozen][freeze]*. +> *[frozen][api-freeze]*. > 👉 **Note**: `runSync` performs the [run phase][overview], not other phases. ###### Parameters * `tree` ([`Node`][node]) — tree to transform and inspect -* `file` ([`VFile`][vfile], optional) — any value accepted as `x` in - `new VFile(x)` +* `file` ([`Compatible`][vfile-compatible], optional) — file associated + with `node`; any value accepted as `x` in `new VFile(x)` ###### Returns Transformed tree ([`Node`][node]). -### `processor.process(file[, done])` +### `processor.stringify(tree[, file])` -Process the given file as configured on the processor. +Compile a syntax tree. -> 👉 **Note**: `process` freezes the processor if not already -> *[frozen][freeze]*. +> 👉 **Note**: `stringify` freezes the processor if not already +> *[frozen][api-freeze]*. -> 👉 **Note**: `process` performs the [parse, run, and stringify -> phases][overview]. +> 👉 **Note**: `stringify` performs the [stringify phase][overview], not the run +> phase or other phases. ###### Parameters -* `file` ([`VFile`][vfile]) — file; any value accepted as `x` in - `new VFile(x)` -* `done` ([`Function`][process-done], optional) — callback +* `tree` ([`Node`][node]) — tree to compile +* `file` ([`Compatible`][vfile-compatible], optional) — file associated + with `node`; any value accepted as `x` in `new VFile(x)` ###### Returns -Nothing if `done` is given (`undefined`). -A [`Promise`][promise] otherwise. -The promise is rejected with a fatal error or resolved with the processed -file ([`VFile`][vfile]). - -The parsed, transformed, and compiled value is available at -[`file.value`][vfile-value] (see note). +Textual representation of the tree (`Uint8Array` or `string`, see note). -> 👉 **Note**: unified typically compiles by serializing: most -> [compilers][compiler] return `string` (or `Uint8Array`). +> 👉 **Note**: unified typically compiles by serializing: most compilers +> return `string` (or `Uint8Array`). > Some compilers, such as the one configured with -> [`rehype-react`][rehype-react], result in other values (in this case, a React -> tree). -> If you’re using a compiler that does not serialize, the result is available -> at `file.result`. +> [`rehype-react`][rehype-react], return other values (in this case, a +> React tree). +> If you’re using a compiler that doesn’t serialize, expect different +> result values. +> +> To register custom results in TypeScript, add them to +> [`CompileResultMap`][api-compile-result-map]. ###### Example -This example shows how `process` can be used to process a file: +This example shows how `stringify` can be used to serialize a syntax tree: ```js -import {unified} from 'unified' -import remarkParse from 'remark-parse' -import remarkRehype from 'remark-rehype' -import rehypeDocument from 'rehype-document' -import rehypeFormat from 'rehype-format' +import {h} from 'hastscript' import rehypeStringify from 'rehype-stringify' +import {unified} from 'unified' -const file = await unified() - .use(remarkParse) - .use(remarkRehype) - .use(rehypeDocument, {title: '👋🌍'}) - .use(rehypeFormat) - .use(rehypeStringify) - .process('# Hello world!') +const tree = h('h1', 'Hello world!') -console.log(String(file)) +const doc = unified().use(rehypeStringify).stringify(tree) + +console.log(doc) ``` Yields: ```html - - - - - 👋🌍 - - - -

Hello world!

- - +

Hello world!

``` -#### `function done(err, file)` +### `processor.use(plugin[, options])` -Callback called when the process is done. +Configure the processor to use a plugin, a list of usable values, or a preset. -Called with either an error or a result. +If the processor is already using a plugin, the previous plugin configuration +is changed based on the options that are passed in. +In other words, the plugin is not added a second time. + +> 👉 **Note**: `use` cannot be called on [*frozen*][api-freeze] processors. +> Call the processor first to create a new unfrozen processor. + +###### Signatures + +* `processor.use(preset?)` +* `processor.use(list)` +* `processor.use(plugin[, ...parameters])` ###### Parameters -* `err` (`Error`, optional) — fatal error -* `file` ([`VFile`][vfile]) — processed file +* `preset` ([`Preset`][api-preset]) — plugins and settings +* `list` ([`PluggableList`][api-pluggable-list]) — list of usable things +* `plugin` ([`Plugin`][api-plugin]) — plugin +* `parameters` (`Array`) — configuration for `plugin`, typically a + single options object + +###### Returns + +Current processor ([`processor`][api-processor]). ###### Example -This example shows how `process` can be used to process a file with a callback. +There are many ways to pass plugins to `.use()`. +This example gives an overview: ```js import {unified} from 'unified' -import remarkParse from 'remark-parse' -import remarkGithub from 'remark-github' -import remarkStringify from 'remark-stringify' -import {reporter} from 'vfile-reporter' unified() - .use(remarkParse) - .use(remarkGithub) - .use(remarkStringify) - .process('@unifiedjs', function (error, file) { - console.error(reporter(error || file)) - if (file) { - console.log(String(file)) - } - }) + // Plugin with options: + .use(pluginA, {x: true, y: true}) + // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): + .use(pluginA, {y: false, z: true}) + // Plugins: + .use([pluginB, pluginC]) + // Two plugins, the second with options: + .use([pluginD, [pluginE, {}]]) + // Preset with plugins and settings: + .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}}) + // Settings only: + .use({settings: {position: false}}) ``` -Yields: +### `CompileResultMap` -```txt -no issues found +Interface of known results from compilers (TypeScript type). + +Normally, compilers result in text ([`Value`][vfile-value] of `vfile`). +When you compile to something else, such as a React node (as in, +`rehype-react`), you can augment this interface to include that type. + +```ts +import type {ReactNode} from 'somewhere' + +declare module 'unified' { + interface CompileResultMap { + // Register a new result (value is used, key should match it). + ReactNode: ReactNode + } +} + +export {} // You may not need this, but it makes sure the file is a module. ``` -```markdown -[**@unifiedjs**](https://github.com/unifiedjs) +Use [`CompileResults`][api-compile-results] to access the values. + +###### Type + +```ts +interface CompileResultMap { + // Note: if `Value` from `VFile` is changed, this should too. + Uint8Array: Uint8Array + string: string +} ``` -### `processor.processSync(file)` +### `CompileResults` -Process the given file as configured on the processor. -An error is thrown if asynchronous transforms are configured. +Acceptable results from compilers (TypeScript type). -> 👉 **Note**: `processSync` freezes the processor if not already -> *[frozen][freeze]*. +To register custom results, add them to +[`CompileResultMap`][api-compile-result-map]. -> 👉 **Note**: `processSync` performs the [parse, run, and stringify -> phases][overview]. +###### Type -###### Parameters +```ts +type CompileResults = CompileResultMap[keyof CompileResultMap] +``` -* `file` ([`VFile`][vfile]) — any value accepted as `x` in `new VFile(x)` +### `Compiler` -###### Returns +A **compiler** handles the compiling of a syntax tree to something else +(in most cases, text) (TypeScript type). -The processed file ([`VFile`][vfile]). +It is used in the stringify phase and called with a [`Node`][node] +and [`VFile`][vfile] representation of the document to compile. -The parsed, transformed, and compiled value is available at -[`file.value`][vfile-value] (see note). +`Compiler` can be a normal function, in which case it should return the +textual representation of the given tree (typically `string`). -> 👉 **Note**: unified typically compiles by serializing: most -> [compilers][compiler] return `string` (or `Uint8Array`). +`Compiler` can also be a constructor function (a function with a `compile` +field in its `prototype`), in which case it is constructed with `new`. +Instances must have a `compile` method that is called without arguments +and typically returns a `string`. + +> 👉 **Note**: unified typically compiles by serializing: most compilers +> return `string` (or `Uint8Array`). > Some compilers, such as the one configured with -> [`rehype-react`][rehype-react], result in other values (in this case, a React -> tree). -> If you’re using a compiler that does not serialize, the result is available -> at `file.result`. +> [`rehype-react`][rehype-react], return other values (in this case, a +> React tree). +> If you’re using a compiler that doesn’t serialize, expect different +> result values. +> +> To register custom results in TypeScript, add them to +> [`CompileResultMap`][api-compile-result-map]. + +###### Type + +```ts +type Compiler< + Tree extends Node = Node, + Result extends CompileResults = CompileResults +> = CompilerClass | CompilerFunction +``` -###### Example +See [`CompilerClass`][api-compiler-class] and +[`CompilerFunction`][api-compiler-function] for more info. -This example shows how `processSync` can be used to process a file, if all -transformers are synchronous. +### `CompilerClass` -```js -import {unified} from 'unified' -import remarkParse from 'remark-parse' -import remarkRehype from 'remark-rehype' -import rehypeDocument from 'rehype-document' -import rehypeFormat from 'rehype-format' -import rehypeStringify from 'rehype-stringify' +Class to compile trees (TypeScript type). -const processor = unified() - .use(remarkParse) - .use(remarkRehype) - .use(rehypeDocument, {title: '👋🌍'}) - .use(rehypeFormat) - .use(rehypeStringify) +###### Type -console.log(String(processor.processSync('# Hello world!'))) +```ts +type CompilerClass< + Tree extends Node = Node, + Result extends CompileResults = CompileResults +> = { + new (tree: Tree, file: VFile): CompilerClass['prototype'] + prototype: { + compile(): Result + } +} ``` -Yields: +### `CompilerFunction` -```html - - - - - 👋🌍 - - - -

Hello world!

- - +Regular function to compile a tree (TypeScript type). + +###### Type + +```ts +type CompilerFunction< + Tree extends Node = Node, + Result extends CompileResults = CompileResults +> = (tree: Tree, file: VFile) => Result ``` -### `processor.data([key[, value]])` +### `Data` -Configure the processor with info available to all plugins. -Information is stored in an object. +Interface of known data that can be supported by all plugins (TypeScript type). Typically, options can be given to a specific plugin, but sometimes it makes sense to have information shared with several plugins. For example, a list of HTML elements that are self-closing, which is needed -during all [phases][overview]. +during all phases. -> 👉 **Note**: setting information cannot occur on *[frozen][freeze]* -> processors. -> Call the processor first to create a new unfrozen processor. +To type this, do something like: -###### Signatures +```ts +declare module 'unified' { + interface Data { + htmlVoidElements?: Array | undefined + } +} -* `processor = processor.data(key, value)` -* `processor = processor.data(values)` -* `value = processor.data(key)` -* `info = processor.data()` +export {} // You may not need this, but it makes sure the file is a module. +``` -###### Parameters +###### Type -* `key` (`string`, optional) — identifier -* `value` (`*`, optional) — value to set -* `values` (`Object`, optional) — values to set +```ts +interface Data { + settings?: Settings | undefined +} +``` -###### Returns +See [`Settings`][api-settings] for more info. -* `processor` — when setting, the processor that `data` is called on -* `value` (`*`) — when getting, the value at `key` -* `info` (`Object`) — without arguments, the key-value store +### `Parser` -###### Example +A **parser** handles the parsing of text to a syntax tree (TypeScript type). -This example show how to get and set info: +It is used in the parse phase and is called with a `string` and +[`VFile`][vfile] of the document to parse. -```js -import {unified} from 'unified' +`Parser` can be a normal function, in which case it must return the syntax +tree representation of the given file ([`Node`][node]). -const processor = unified().data('alpha', 'bravo') +`Parser` can also be a constructor function (a function with a `parse` +field in its `prototype`), in which case it is constructed with `new`. +Instances must have a `parse` method that is called without arguments and +must return a [`Node`][node]. -processor.data('alpha') // => 'bravo' +###### Type -processor.data() // => {alpha: 'bravo'} +```ts +type Parser = ParserClass | ParserFunction +``` -processor.data({charlie: 'delta'}) +See [`ParserClass`][api-parser-class] and +[`ParserFunction`][api-parser-function] for more info. -processor.data() // => {charlie: 'delta'} -``` +### `ParserClass` -### `processor.freeze()` +Class to parse files (TypeScript type). -Freeze a processor. +###### Type -Frozen processors are meant to be extended and not to be configured directly. +```ts +type ParserClass = { + new (document: string, file: VFile): ParserClass['prototype'] + prototype: { + parse(): Tree + } +} +``` -When a processor is frozen it cannot be unfrozen. -New processors working the same way can be created by calling the processor. +### `ParserFunction` -It’s possible to freeze processors explicitly by calling `.freeze()`. -Processors freeze automatically when [`.parse()`][parse], [`.run()`][run], -[`.runSync()`][run-sync], [`.stringify()`][stringify], [`.process()`][process], -or [`.processSync()`][process-sync] are called. +Regular function to parse a file (TypeScript type). -###### Returns +###### Type -The processor that `freeze` was called on (`processor`). +```ts +type ParserFunction = (document: string, file: VFile) => Tree +``` -###### Example +### `Pluggable` -This example, `index.js`, shows how `rehype` prevents extensions to itself: +Union of the different ways to add plugins and settings (TypeScript type). -```js -import {unified} from 'unified' -import rehypeParse from 'rehype-parse' -import rehypeStringify from 'rehype-stringify' +###### Type -export const rehype = unified().use(rehypeParse).use(rehypeStringify).freeze() +```ts +type Pluggable = + | Plugin, any, any> + | PluginTuple, any, any> + | Preset ``` -That processor can be used and configured like so: +See [`Plugin`][api-plugin], [`PluginTuple`][api-plugin-tuple], +and [`Preset`][api-preset] for more info. -```js -import {rehype} from 'rehype' -import rehypeFormat from 'rehype-format' -// … +### `PluggableList` -rehype() - .use(rehypeFormat) - // … -``` +List of plugins and presets (TypeScript type). -A similar looking example is broken as operates on the frozen interface. -If this behavior was allowed it would result in unexpected behavior so an error -is thrown. -**This is not valid**: - -```js -import {rehype} from 'rehype' -import rehypeFormat from 'rehype-format' -// … +###### Type -rehype - .use(rehypeFormat) - // … +```ts +type PluggableList = Array ``` -Yields: - -```txt -~/node_modules/unified/index.js:426 - throw new Error( - ^ +See [`Pluggable`][api-pluggable] for more info. -Error: Cannot call `use` on a frozen processor. -Create a new processor first, by calling it: use `processor()` instead of `processor`. - at assertUnfrozen (~/node_modules/unified/index.js:426:11) - at Function.use (~/node_modules/unified/index.js:165:5) - … -``` +### `Plugin` -## `Plugin` +Single plugin (TypeScript type). -**Plugins** configure the processors they are applied on in the following ways: +Plugins configure the processors they are applied on in the following ways: -* they change the processor, such as the [parser][], the [compiler][], or by - configuring [data][] +* they change the processor, such as the parser, the compiler, or by + configuring data * they specify how to handle trees and files -Plugins are a concept. -They materialize as [`Attacher`s][attacher]. +In practise, they are functions that can receive options and configure the +processor (`this`). + +> 👉 **Note**: plugins are called when the processor is *frozen*, not when they +> are applied. + +###### Type + +```ts +type Plugin< + PluginParameters extends unknown[] = [], + Input extends Node | string | undefined = Node, + Output = Input +> = ( + this: Processor, + ...parameters: PluginParameters +) => Input extends string // Parser. + ? Output extends Node | undefined + ? undefined | void + : never + : Output extends CompileResults // Compiler. + ? Input extends Node | undefined + ? undefined | void + : never + : // Inspect/transform. + | Transformer< + Input extends Node ? Input : Node, + Output extends Node ? Output : Node + > + | undefined + | void +``` + +See [`Transformer`][api-transformer] for more info. ###### Example @@ -1008,7 +1220,7 @@ export function move(options) { throw new Error('Missing `options.extname`') } - return function (tree, file) { + return function (_, file) { if (file.extname && file.extname !== options.extname) { file.extname = options.extname } @@ -1016,21 +1228,21 @@ export function move(options) { } ``` -`index.md`: +`example.md`: ```markdown # Hello, world! ``` -`index.js`: +`example.js`: ```js -import {read, write} from 'to-vfile' -import {reporter} from 'vfile-reporter' -import {unified} from 'unified' +import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' -import rehypeStringify from 'rehype-stringify' +import {read, write} from 'to-vfile' +import {unified} from 'unified' +import {reporter} from 'vfile-reporter' import {move} from './move.js' const file = await unified() @@ -1038,103 +1250,71 @@ const file = await unified() .use(remarkRehype) .use(move, {extname: '.html'}) .use(rehypeStringify) - .process(await read('index.md')) + .process(await read('example.md')) console.error(reporter(file)) -await write(file) // Written to `index.html`. - +await write(file) // Written to `example.html`. ``` Yields: ```txt -index.md: no issues found +example.md: no issues found ``` -…and in `index.html`: +…and in `example.html`: ```html

Hello, world!

``` -### `function attacher(options?)` - -Attachers are materialized plugins. -They are functions that can receive options and configure the processor. +### `PluginTuple` -Attachers change the processor, such as the [parser][], the [compiler][], -by configuring [data][], or by specifying how the tree and file are handled. +Tuple of a plugin and its configuration (TypeScript type). -> 👉 **Note**: attachers are called when the processor is *[frozen][freeze]*, -> not when they are applied. +The first item is a plugin, the rest are its parameters. -###### Parameters - -* `this` (`processor`) — processor the attacher is applied to -* `options` (`*`, optional) — configuration - -###### Returns - -Optional transform ([`Transformer`][transformer]). - -### `function transformer(tree, file[, next])` - -Transformers handle syntax trees and files. - -They are functions that are called each time a syntax tree and file are passed -through the [run phase][overview]. -When an error occurs in them (either because it’s thrown, returned, rejected, -or passed to [`next`][next]), the process stops. - -The run phase is handled by [`trough`][trough], see its documentation for the -exact semantics of these functions. - -###### Parameters - -* `tree` ([`Node`][node]) — tree to handle -* `file` ([`VFile`][vfile]) — file to handle -* `next` ([`Function`][next], optional) — callback - -###### Returns +###### Type -If you accept `next`, nothing. -Otherwise: - -* `Error` — fatal error to stop the process -* `Promise` or `undefined` — the next transformer keeps using same - tree -* `Promise` or [`Node`][node] — new, changed, tree +```ts +type PluginTuple< + TupleParameters extends unknown[] = [], + Input extends Node | string | undefined = undefined, + Output = undefined +> = [ + plugin: Plugin, + ...parameters: TupleParameters +] +``` -#### `function next(err[, tree[, file]])` +See [`Plugin`][api-plugin] for more info. -If the signature of a `transformer` accepts a third argument, the transformer -may perform asynchronous operations, and must call `next()`. +### `Preset` -###### Parameters +Sharable configuration (TypeScript type). -* `err` (`Error`, optional) — fatal error to stop the process -* `tree` ([`Node`][node], optional) — new, changed, tree -* `file` ([`VFile`][vfile], optional) — new, changed, file - -## `Preset` +They can contain plugins and settings. -Presets are sharable configuration. +###### Fields -They can contain plugins and settings. +* `plugins` ([`PluggableList`][api-pluggable-list], optional) + — list of plugins and presets +* `settings` ([`Data`][api-data], optional) + — shared settings for parsers and compilers ###### Example `preset.js`: ```js -import remarkPresetLintRecommended from 'remark-preset-lint-recommended' -import remarkPresetLintConsistent from 'remark-preset-lint-consistent' import remarkCommentConfig from 'remark-comment-config' -import remarkToc from 'remark-toc' import remarkLicense from 'remark-license' +import remarkPresetLintConsistent from 'remark-preset-lint-consistent' +import remarkPresetLintRecommended from 'remark-preset-lint-recommended' +import remarkToc from 'remark-toc' -export const preset = { - settings: {bullet: '*', emphasis: '*', fences: true}, +/** @type {import('unified').Preset} */ +const preset = { plugins: [ remarkPresetLintRecommended, remarkPresetLintConsistent, @@ -1142,7 +1322,10 @@ export const preset = { [remarkToc, {maxDepth: 3, tight: true}], remarkLicense ] + settings: {bullet: '*', emphasis: '*', fences: true}, } + +export default preset ``` `example.md`: @@ -1159,13 +1342,13 @@ _Emphasis_ and **importance**. ## License ``` -`index.js`: +`example.js`: ```js -import {read, write} from 'to-vfile' import {remark} from 'remark' +import {read, write} from 'to-vfile' import {reporter} from 'vfile-reporter' -import {preset} from './preset.js' +import preset from './preset.js' const file = await remark() .use(preset) @@ -1200,51 +1383,188 @@ example.md: no issues found [MIT](license) © [Titus Wormer](https://wooorm.com) ``` +### `ProcessCallback` + +Callback called when the process is done (TypeScript type). + +Called with either an error or a result. + +###### Parameters + +* `error` (`Error`, optional) + — fatal error +* `file` ([`VFile`][vfile], optional) + — processed file + +###### Returns + +Nothing (`undefined`). + +###### Example + +This example shows how `process` can be used to process a file with a callback. + +```js +import remarkGithub from 'remark-github' +import remarkParse from 'remark-parse' +import remarkStringify from 'remark-stringify' +import {unified} from 'unified' +import {reporter} from 'vfile-reporter' + +unified() + .use(remarkParse) + .use(remarkGithub) + .use(remarkStringify) + .process('@unifiedjs', function (error, file) { + if (error) throw error + if (file) { + console.error(reporter(file)) + console.log(String(file)) + } + }) +``` + +Yields: + +```txt +no issues found +``` + +```markdown +[**@unifiedjs**](https://github.com/unifiedjs) +``` + +### `Processor` + +Type of a [`processor`][api-processor] (TypeScript type). + +### `RunCallback` + +Callback called when transformers are done (TypeScript type). + +Called with either an error or results. + +###### Parameters + +* `error` (`Error`, optional) + — fatal error +* `tree` ([`Node`][node], optional) + — transformed tree +* `file` ([`VFile`][vfile], optional) + — file + +###### Returns + +Nothing (`undefined`). + +### `Settings` + +Interface of known extra options, that can be supported by parser and +compilers. + +This exists so that users can use packages such as `remark`, which configure +both parsers and compilers (in this case `remark-parse` and +`remark-stringify`), and still provide options for them. + +When you make parsers or compilers, that could be packaged up together, you +should support `this.data('settings')` as input and merge it with explicitly +passed `options`. +Then, to type it, using `remark-stringify` as an example, do something like: + +```ts +declare module 'unified' { + interface Settings { + bullet: '*' | '+' | '-' + // … + } +} + +export {} // You may not need this, but it makes sure the file is a module. +``` + +###### Type + +```ts +interface Settings {} +``` + +### `TransformCallback` + +Callback passed to transforms (TypeScript type). + +If the signature of a `transformer` accepts a third argument, the transformer +may perform asynchronous operations, and must call it. + +###### Parameters + +* `error` (`Error`, optional) + — fatal error to stop the process +* `tree` ([`Node`][node], optional) + — new, changed, tree +* `file` ([`VFile`][vfile], optional) + — new, changed, file + +###### Returns + +Nothing (`undefined`). + +### `Transformer` + +Transformers handle syntax trees and files (TypeScript type). + +They are functions that are called each time a syntax tree and file are +passed through the run phase. +When an error occurs in them (either because it’s thrown, returned, +rejected, or passed to `next`), the process stops. + +The run phase is handled by [`trough`][trough], see its documentation for +the exact semantics of these functions. + +> 👉 **Note**: you should likely ignore `next`: don’t accept it. +> it supports callback-style async work. +> But promises are likely easier to reason about. + +###### Type + +```ts +type Transformer< + Input extends Node = Node, + Output extends Node = Input +> = ( + tree: Input, + file: VFile, + next: TransformCallback +) => + | Promise + | Output + | Error + | undefined +``` + ## Types This package is fully typed with [TypeScript][]. -It exports the following additional types: - -* `Processor` - — processor, where `ParseTree` is the tree that the parser creates, - `CurrentTree` the tree that the current plugin yields, `CompileTree` the - tree that the compiler accepts, and `CompileResult` the thing that the - compiler yields -* `FrozenProcessor` - — like `Processor` but frozen -* `Plugin` - — plugin, where `PluginParameters` are the accepted arguments, `Input` the - input value, and `Output` the output value (see below) -* `Pluggable` -* `Preset` - — preset -* `PluginTuple` - — plugin tuple -* `Pluggable` - — any usable value, where `PluginParameters` are the accepted arguments -* `PluggableList` - — list of plugins and presets -* `Transformer` - — transformer, where `Input` and `Output` are the input/output trees -* `TransformCallback` - — third argument of a transformer -* `Parser` - — parser as a class or normal function, where `Tree` is the resulting tree -* `ParserClass` - — parser class -* `ParserFunction` - — parser function -* `Compiler` - — compiler as a class or normal function, where `Tree` is the accepted tree - and `Result` the thing that the compiler yields -* `CompilerClass` - — compiler class -* `CompilerFunction` - — compiler function -* `RunCallback` - — callback to `run`, where `Tree` is the resulting tree -* `ProcessCallback` - — callback to `process`, where `File` is the resulting file +It exports the additional types +[`CompileResultMap`][api-compile-result-map], +[`CompileResults`][api-compile-results], +[`Compiler`][api-compiler], +[`CompilerClass`][api-compiler-class], +[`CompilerFunction`][api-compiler-function], +[`Data`][api-data], +[`Parser`][api-parser], +[`ParserClass`][api-parser-class], +[`ParserFunction`][api-parser-function], +[`Pluggable`][api-pluggable], +[`PluggableList`][api-pluggable-list], +[`Plugin`][api-plugin], +[`PluginTuple`][api-plugin-tuple], +[`Preset`][api-preset], +[`ProcessCallback`][api-process-callback], +[`Processor`][api-processor], +[`RunCallback`][api-run-callback], +[`Settings`][api-settings], +[`TransformCallback`][api-transform-callback], +and [`Transformer`][api-transformer] For TypeScript to work, it is particularly important to type your plugins correctly. @@ -1255,22 +1575,25 @@ node types for the syntax trees provided by our packages (as in, ```js /** - * @typedef {import('mdast').Root} MdastRoot * @typedef {import('hast').Root} HastRoot - * + * @typedef {import('mdast').Root} MdastRoot + */ + +/** * @typedef Options * Configuration (optional). - * @property {boolean} [someField] + * @property {boolean | null | undefined} [someField] * Some option (optional). */ // To type options: -/** @type {import('unified').Plugin<[Options?]>} */ +/** @type {import('unified').Plugin<[(Options | null | undefined)?]>} */ export function myPluginAcceptingOptions(options) { - // `options` is `Options?`. + const settings = options || {} + // `settings` is now `Options`. } -// To type a plugin that works on a certain tree: +// To type a plugin that works on a certain tree, without options: /** @type {import('unified').Plugin<[], MdastRoot>} */ export function myRemarkPlugin() { return function (tree, file) { @@ -1298,10 +1621,13 @@ export function rehypeStringify(options) {} ## Compatibility -Projects maintained by the unified collective are compatible with all maintained +Projects maintained by the unified collective are compatible with maintained versions of Node.js. -As of now, that is Node.js 12.20+, 14.14+, and 16.0+. -Our projects sometimes work with older versions, but this is not guaranteed. + +When we cut a new major release, we drop support for unmaintained versions of +Node. +This means we try to keep the current release line, `unified@^10`, compatible +with Node.js 12. ## Contribute @@ -1422,9 +1748,9 @@ work on [`ware`][ware], as it was a huge initial inspiration. [downloads]: https://www.npmjs.com/package/unified -[size-badge]: https://img.shields.io/bundlephobia/minzip/unified.svg +[size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=unified -[size]: https://bundlephobia.com/result?p=unified +[size]: https://bundlejs.com/?q=unified [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg @@ -1478,10 +1804,10 @@ work on [`ware`][ware], as it was a huge initial inspiration. [nlcst]: https://github.com/syntax-tree/nlcst -[xast]: https://github.com/syntax-tree/xast - [unist]: https://github.com/syntax-tree/unist +[xast]: https://github.com/syntax-tree/xast + [unified-engine]: https://github.com/unifiedjs/unified-engine [unified-args]: https://github.com/unifiedjs/unified-args @@ -1492,98 +1818,108 @@ work on [`ware`][ware], as it was a huge initial inspiration. [unified-stream]: https://github.com/unifiedjs/unified-stream -[remark-rehype]: https://github.com/remarkjs/remark-rehype - -[remark-retext]: https://github.com/remarkjs/remark-retext +[rehype-remark]: https://github.com/rehypejs/rehype-remark [rehype-retext]: https://github.com/rehypejs/rehype-retext -[rehype-remark]: https://github.com/rehypejs/rehype-remark +[remark-rehype]: https://github.com/remarkjs/remark-rehype + +[remark-retext]: https://github.com/remarkjs/remark-retext [node]: https://github.com/syntax-tree/unist#node [vfile]: https://github.com/vfile/vfile -[vfile-value]: https://github.com/vfile/vfile#vfilevalue +[vfile-compatible]: https://github.com/vfile/vfile#compatible + +[vfile-value]: https://github.com/vfile/vfile#value [vfile-utilities]: https://github.com/vfile/vfile#list-of-utilities -[overview]: #overview +[rehype-react]: https://github.com/rehypejs/rehype-react -[file]: #file +[trough]: https://github.com/wooorm/trough#function-fninput-next -[api]: #api +[rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins -[process]: #processorprocessfile-done +[remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins -[process-sync]: #processorprocesssyncfile +[retext-plugins]: https://github.com/retextjs/retext/blob/main/doc/plugins.md#list-of-plugins -[parse]: #processorparsefile +[awesome-rehype]: https://github.com/rehypejs/awesome-rehype -[parser]: #processorparser +[awesome-remark]: https://github.com/remarkjs/awesome-remark -[stringify]: #processorstringifytree-file +[awesome-retext]: https://github.com/retextjs/awesome-retext -[run]: #processorruntree-file-done +[topic-rehype-plugin]: https://github.com/topics/rehype-plugin -[run-sync]: #processorrunsynctree-file +[topic-remark-plugin]: https://github.com/topics/remark-plugin -[compiler]: #processorcompiler +[topic-retext-plugin]: https://github.com/topics/retext-plugin -[data]: #processordatakey-value +[types-hast]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/hast -[attacher]: #function-attacheroptions +[types-mdast]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mdast -[transformer]: #function-transformertree-file-next +[types-nlcst]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nlcst -[next]: #function-nexterr-tree-file +[preliminary]: https://github.com/retextjs/retext/commit/8fcb1f -[freeze]: #processorfreeze +[externalised]: https://github.com/remarkjs/remark/commit/9892ec -[plugin]: #plugin +[published]: https://github.com/unifiedjs/unified/commit/2ba1cf -[run-done]: #function-doneerr-tree-file +[ware]: https://github.com/segmentio/ware -[process-done]: #function-doneerr-file +[api]: #api [contribute]: #contribute +[overview]: #overview + [sponsor]: #sponsor -[rehype-react]: https://github.com/rehypejs/rehype-react +[api-compile-result-map]: #compileresultmap -[trough]: https://github.com/wooorm/trough#function-fninput-next +[api-compile-results]: #compileresults -[promise]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise +[api-compiler]: #compiler -[remark-plugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins +[api-compiler-class]: #compilerclass -[rehype-plugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins +[api-compiler-function]: #compilerfunction -[retext-plugins]: https://github.com/retextjs/retext/blob/main/doc/plugins.md#list-of-plugins +[api-data]: #data -[awesome-remark]: https://github.com/remarkjs/awesome-remark +[api-freeze]: #processorfreeze -[awesome-rehype]: https://github.com/rehypejs/awesome-rehype +[api-parser]: #parser -[awesome-retext]: https://github.com/retextjs/awesome-retext +[api-parser-class]: #parserclass -[topic-remark-plugin]: https://github.com/topics/remark-plugin +[api-parser-function]: #parserfunction -[topic-rehype-plugin]: https://github.com/topics/rehype-plugin +[api-pluggable]: #pluggable -[topic-retext-plugin]: https://github.com/topics/retext-plugin +[api-pluggable-list]: #pluggablelist -[types-hast]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/hast +[api-plugin]: #plugin -[types-mdast]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mdast +[api-plugin-tuple]: #plugintuple -[types-nlcst]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nlcst +[api-preset]: #preset -[preliminary]: https://github.com/retextjs/retext/commit/8fcb1f#diff-168726dbe96b3ce427e7fedce31bb0bc +[api-process]: #processorprocessfile-done -[externalised]: https://github.com/remarkjs/remark/commit/9892ec#diff-168726dbe96b3ce427e7fedce31bb0bc +[api-process-callback]: #processcallback -[published]: https://github.com/unifiedjs/unified/commit/2ba1cf +[api-processor]: #processor -[ware]: https://github.com/segmentio/ware +[api-run-callback]: #runcallback + +[api-settings]: #settings + +[api-transform-callback]: #transformcallback + +[api-transformer]: #transformer diff --git a/test/types.d.ts b/test/types.d.ts index 5c293242..b83ae37d 100644 --- a/test/types.d.ts +++ b/test/types.d.ts @@ -1,7 +1,10 @@ declare module 'unified' { interface Data { + alpha?: boolean | undefined + bar?: boolean | undefined baz?: 'qux' | undefined - foo?: 'bar' | undefined + foo?: 'bar' | boolean | undefined + qux?: boolean | undefined x?: boolean | undefined } } From 8e574788d7c6a44b6b2fe884bd42c89580ef4643 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 20:02:33 +0200 Subject: [PATCH 42/46] Remove support for classes as compilers, parsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class versions have not been used in ages. Support for them is pretty complex for something that isn’t used. So, let’s make everything smaller for everyone instead. And the alternative is fine: ```js class Parser {} // Say this is your previous class, and you still want classes. processor.parser = function (doc, file) { return new Parser(doc, file).parse() } ``` …same with compilers. You also should set `compiler` and `parser` instead of `Compiler` and `Parser`. The old version is still supported but will be removed. This also drops the `CompilerClass`, `CompilerFunction`, `ParserClass`, and `ParserFunction` types. Use `Compiler` and `Parser` instead. --- index.d.ts | 4 - index.test-d.ts | 17 +--- lib/index.js | 205 ++++++++++---------------------------- readme.md | 114 +++------------------ test/freeze.js | 4 +- test/parse.js | 65 +----------- test/process-compilers.js | 12 +-- test/process-sync.js | 22 ++-- test/process.js | 22 ++-- test/stringify.js | 58 +---------- 10 files changed, 102 insertions(+), 421 deletions(-) diff --git a/index.d.ts b/index.d.ts index 477f04f6..45d03891 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,12 +5,8 @@ export type { // `CompileResultMap` is typed and exposed below. CompileResults, Compiler, - CompilerClass, - CompilerFunction, // `Data` is typed and exposed below. Parser, - ParserClass, - ParserFunction, Pluggable, PluggableList, Plugin, diff --git a/index.test-d.ts b/index.test-d.ts index 05a5a226..d92e1483 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -187,31 +187,18 @@ unified() unified().use(function () { // Function. - this.Parser = function (doc, file) { + this.parser = function (doc, file) { expectType(doc) expectType(file) return {type: ''} } - // Class. - this.Parser = class { - parse() { - return {type: 'x'} - } - } - // Function. - this.Compiler = function (tree, file) { + this.compiler = function (tree, file) { expectType(tree) expectType(file) return '' } - - this.Compiler = class { - compile() { - return '' - } - } }) // # Plugins w/ transformer diff --git a/lib/index.js b/lib/index.js index f0072c5f..0fde9644 100644 --- a/lib/index.js +++ b/lib/index.js @@ -23,20 +23,14 @@ * The node that the compiler receives (default: `Node`). * @template {CompileResults} [Result=CompileResults] * The thing that the compiler yields (default: `CompileResults`). - * @typedef {CompilerClass | CompilerFunction} Compiler + * @callback Compiler * A **compiler** handles the compiling of a syntax tree to something else - * (in most cases, text). + * (in most cases, text) (TypeScript type). * * It is used in the stringify phase and called with a {@link Node `Node`} * and {@link VFile `VFile`} representation of the document to compile. - * - * `Compiler` can be a normal function, in which case it should return the - * textual representation of the given tree (typically `string`). - * - * `Compiler` can also be a constructor function (a function with a `compile` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `compile` method that is called without arguments - * and typically returns a `string`. + * It should return the textual representation of the given tree (typically + * `string`). * * > 👉 **Note**: unified typically compiles by serializing: most compilers * > return `string` (or `Uint8Array`). @@ -50,27 +44,6 @@ * > {@link CompileResultMap `CompileResultMap`}. * * [rehype-react]: https://github.com/rehypejs/rehype-react - */ - -/** - * @template {Node} [Tree=Node] - * The node that the compiler receives (default: `Node`) - * @template {CompileResults} [Result=CompileResults] - * The thing that the compiler yields (default: `CompileResults`). - * @typedef {({ - * prototype: {compile(): Result} - * new (tree: Tree, file: VFile): CompilerClass['prototype'] - * })} CompilerClass - * Class to compile trees. - */ - -/** - * @template {Node} [Tree=Node] - * The node that the compiler receives (default: `Node`). - * @template {CompileResults} [Result=CompileResults] - * The thing that the compiler yields (default: `CompileResults`). - * @callback CompilerFunction - * Regular function to compile a tree. * @param {Tree} tree * Tree to compile. * @param {VFile} file @@ -83,36 +56,13 @@ /** * @template {Node} [Tree=Node] * The node that the parser yields (default: `Node`) - * @typedef {ParserClass | ParserFunction} Parser + * @callback Parser * A **parser** handles the parsing of text to a syntax tree. * * It is used in the parse phase and is called with a `string` and * {@link VFile `VFile`} of the document to parse. - * - * `Parser` can be a normal function, in which case it must return the syntax - * tree representation of the given file ({@link Node `Node`}). - * - * `Parser` can also be a constructor function (a function with a `parse` - * field in its `prototype`), in which case it is constructed with `new`. - * Instances must have a `parse` method that is called without arguments and - * must return a {@link Node `Node`}. - */ - -/** - * @template {Node} [Tree=Node] - * The node that the parser yields (default: `Node`). - * @typedef {({ - * prototype: {parse(): Tree} - * new (document: string, file: VFile): ParserClass['prototype'] - * })} ParserClass - * Class to parse files. - */ - -/** - * @template {Node} [Tree=Node] - * The node that the parser yields (default: `Node`). - * @callback ParserFunction - * Regular function to parse a file. + * It must return the syntax tree representation of the given file + * ({@link Node `Node`}). * @param {string} document * Document to parse. * @param {VFile} file @@ -403,13 +353,11 @@ import {trough} from 'trough' import {VFile} from 'vfile' import {CallableInstance} from './callable-instance.js' -// To do: we could drop class support for the parser and compiler, -// we don’t use that anymore? -// Would be less breaking if we do `processor.compiler || processor.Compiler`. +// To do: next major: drop `Compiler`, `Parser`: prefer lowercase. // To do: we could start yielding `never` in TS when a parser is missing and // `parse` is called. -// Currently, we allow directly setting `processor.Parser`, which is untyped. +// Currently, we allow directly setting `processor.parser`, which is untyped. const own = {}.hasOwnProperty @@ -435,8 +383,10 @@ export class Processor extends CallableInstance { super('copy') /** - * Compiler to use. + * Compiler to use (deprecated). * + * @deprecated + * Use `compiler` instead. * @type {( * Compiler< * CompileTree extends undefined ? Node : CompileTree, @@ -448,8 +398,10 @@ export class Processor extends CallableInstance { this.Compiler = undefined /** - * Parser to use. + * Parser to use (deprecated). * + * @deprecated + * Use `parser` instead. * @type {( * Parser | * undefined @@ -470,6 +422,19 @@ export class Processor extends CallableInstance { */ this.attachers = [] + /** + * Compiler to use. + * + * @type {( + * Compiler< + * CompileTree extends undefined ? Node : CompileTree, + * CompileResult extends undefined ? CompileResults : CompileResult + * > | + * undefined + * )} + */ + this.compiler = undefined + /** * Internal state to track where we are while freezing. * @@ -497,6 +462,16 @@ export class Processor extends CallableInstance { */ this.namespace = {} + /** + * Parser to use. + * + * @type {( + * Parser | + * undefined + * )} + */ + this.parser = undefined + /** * Internal list of configured transformers. * @@ -687,26 +662,9 @@ export class Processor extends CallableInstance { parse(file) { this.freeze() const realFile = vfile(file) - const Parser = this.Parser - assertParser('parse', Parser) - - if (newable(Parser, 'parse')) { - const ParserClass = - /** @type {ParserClass} */ ( - Parser - ) - - const parserInstace = new ParserClass(String(realFile), realFile) - - return parserInstace.parse() - } - - const parserFunction = - /** @type {ParserFunction} */ ( - Parser - ) - - return parserFunction(String(realFile), realFile) + const parser = this.parser || this.Parser + assertParser('parse', parser) + return parser(String(realFile), realFile) } /** @@ -755,8 +713,8 @@ export class Processor extends CallableInstance { const self = this this.freeze() - assertParser('process', this.Parser) - assertCompiler('process', this.Compiler) + assertParser('process', this.parser || this.Parser) + assertCompiler('process', this.compiler || this.Compiler) return done ? executor(undefined, done) : new Promise(executor) @@ -854,8 +812,8 @@ export class Processor extends CallableInstance { let result this.freeze() - assertParser('processSync', this.Parser) - assertCompiler('processSync', this.Compiler) + assertParser('processSync', this.parser || this.Parser) + assertCompiler('processSync', this.compiler || this.Compiler) this.process(file, realDone) assertDone('processSync', 'process', complete) @@ -1036,35 +994,11 @@ export class Processor extends CallableInstance { stringify(tree, file) { this.freeze() const realFile = vfile(file) - const Compiler = this.Compiler - assertCompiler('stringify', Compiler) + const compiler = this.compiler || this.Compiler + assertCompiler('stringify', compiler) assertNode(tree) - if (newable(Compiler, 'compile')) { - const CompilerClass = /** - * @type {( - * CompilerClass< - * CompileTree extends undefined ? Node : CompileTree, - * CompileResult extends undefined ? CompileResults : CompileResult - * > - * )} - */ (Compiler) - - const compilerInstace = new CompilerClass(tree, realFile) - - return compilerInstace.compile() - } - - const compilerFunction = /** - * @type {( - * CompilerFunction< - * CompileTree extends undefined ? Node : CompileTree, - * CompileResult extends undefined ? CompileResults : CompileResult - * > - * )} - */ (Compiler) - - return compilerFunction(tree, realFile) + return compiler(tree, realFile) } /** @@ -1270,45 +1204,6 @@ export class Processor extends CallableInstance { */ export const unified = new Processor().freeze() -/** - * Check if `value` is a constructor. - * - * @param {unknown} value - * @param {string} name - * @returns {boolean} - */ -function newable(value, name) { - const proto = - // Prototypes are `unknown`. - // type-coverage:ignore-next-line - typeof value === 'function' && /** @type {unknown} */ (value.prototype) - - return ( - proto !== null && - typeof proto === 'object' && - (keys(proto) || name in proto) - ) -} - -/** - * Check if `value` is an object with keys. - * - * @param {object} value - * @returns {boolean} - */ -function keys(value) { - /** @type {string} */ - let key - - for (key in value) { - if (own.call(value, key)) { - return true - } - } - - return false -} - /** * Assert a parser is available. * @@ -1318,7 +1213,7 @@ function keys(value) { */ function assertParser(name, value) { if (typeof value !== 'function') { - throw new TypeError('Cannot `' + name + '` without `Parser`') + throw new TypeError('Cannot `' + name + '` without `parser`') } } @@ -1331,7 +1226,7 @@ function assertParser(name, value) { */ function assertCompiler(name, value) { if (typeof value !== 'function') { - throw new TypeError('Cannot `' + name + '` without `Compiler`') + throw new TypeError('Cannot `' + name + '` without `compiler`') } } diff --git a/readme.md b/readme.md index d727415f..0fc7fe75 100644 --- a/readme.md +++ b/readme.md @@ -19,11 +19,11 @@ * [Overview](#overview) * [API](#api) * [`processor()`](#processor) - * [`processor.Compiler`](#processorcompiler) - * [`processor.Parser`](#processorparser) + * [`processor.compiler`](#processorcompiler) * [`processor.data([key[, value]])`](#processordatakey-value) * [`processor.freeze()`](#processorfreeze) * [`processor.parse(file)`](#processorparsefile) + * [`processor.parser`](#processorparser) * [`processor.process(file[, done])`](#processorprocessfile-done) * [`processor.processSync(file)`](#processorprocesssyncfile) * [`processor.run(tree[, file][, done])`](#processorruntree-file-done) @@ -33,12 +33,8 @@ * [`CompileResultMap`](#compileresultmap) * [`CompileResults`](#compileresults) * [`Compiler`](#compiler) - * [`CompilerClass`](#compilerclass) - * [`CompilerFunction`](#compilerfunction) * [`Data`](#data) * [`Parser`](#parser) - * [`ParserClass`](#parserclass) - * [`ParserFunction`](#parserfunction) * [`Pluggable`](#pluggable) * [`PluggableList`](#pluggablelist) * [`Plugin`](#plugin) @@ -414,14 +410,10 @@ process.stdin.pipe( ) ``` -### `processor.Compiler` +### `processor.compiler` Compiler to use ([`Compiler`][api-compiler], optional). -### `processor.Parser` - -Parser to use ([`Parser`][api-parser], optional). - ### `processor.data([key[, value]])` Configure the processor with info available to all plugins. @@ -593,6 +585,10 @@ Yields: } ``` +### `processor.parser` + +Parser to use ([`Parser`][api-parser], optional). + ### `processor.process(file[, done])` Process the given file as configured on the processor. @@ -986,14 +982,8 @@ A **compiler** handles the compiling of a syntax tree to something else It is used in the stringify phase and called with a [`Node`][node] and [`VFile`][vfile] representation of the document to compile. - -`Compiler` can be a normal function, in which case it should return the -textual representation of the given tree (typically `string`). - -`Compiler` can also be a constructor function (a function with a `compile` -field in its `prototype`), in which case it is constructed with `new`. -Instances must have a `compile` method that is called without arguments -and typically returns a `string`. +It should return the textual representation of the given tree (typically +`string`). > 👉 **Note**: unified typically compiles by serializing: most compilers > return `string` (or `Uint8Array`). @@ -1012,40 +1002,6 @@ and typically returns a `string`. type Compiler< Tree extends Node = Node, Result extends CompileResults = CompileResults -> = CompilerClass | CompilerFunction -``` - -See [`CompilerClass`][api-compiler-class] and -[`CompilerFunction`][api-compiler-function] for more info. - -### `CompilerClass` - -Class to compile trees (TypeScript type). - -###### Type - -```ts -type CompilerClass< - Tree extends Node = Node, - Result extends CompileResults = CompileResults -> = { - new (tree: Tree, file: VFile): CompilerClass['prototype'] - prototype: { - compile(): Result - } -} -``` - -### `CompilerFunction` - -Regular function to compile a tree (TypeScript type). - -###### Type - -```ts -type CompilerFunction< - Tree extends Node = Node, - Result extends CompileResults = CompileResults > = (tree: Tree, file: VFile) => Result ``` @@ -1086,47 +1042,13 @@ A **parser** handles the parsing of text to a syntax tree (TypeScript type). It is used in the parse phase and is called with a `string` and [`VFile`][vfile] of the document to parse. - -`Parser` can be a normal function, in which case it must return the syntax -tree representation of the given file ([`Node`][node]). - -`Parser` can also be a constructor function (a function with a `parse` -field in its `prototype`), in which case it is constructed with `new`. -Instances must have a `parse` method that is called without arguments and -must return a [`Node`][node]. - -###### Type - -```ts -type Parser = ParserClass | ParserFunction -``` - -See [`ParserClass`][api-parser-class] and -[`ParserFunction`][api-parser-function] for more info. - -### `ParserClass` - -Class to parse files (TypeScript type). +It must return the syntax tree representation of the given file +([`Node`][node]). ###### Type ```ts -type ParserClass = { - new (document: string, file: VFile): ParserClass['prototype'] - prototype: { - parse(): Tree - } -} -``` - -### `ParserFunction` - -Regular function to parse a file (TypeScript type). - -###### Type - -```ts -type ParserFunction = (document: string, file: VFile) => Tree +type Parser = (document: string, file: VFile) => Tree ``` ### `Pluggable` @@ -1548,12 +1470,8 @@ It exports the additional types [`CompileResultMap`][api-compile-result-map], [`CompileResults`][api-compile-results], [`Compiler`][api-compiler], -[`CompilerClass`][api-compiler-class], -[`CompilerFunction`][api-compiler-function], [`Data`][api-data], [`Parser`][api-parser], -[`ParserClass`][api-parser-class], -[`ParserFunction`][api-parser-function], [`Pluggable`][api-pluggable], [`PluggableList`][api-pluggable-list], [`Plugin`][api-plugin], @@ -1886,20 +1804,12 @@ work on [`ware`][ware], as it was a huge initial inspiration. [api-compiler]: #compiler -[api-compiler-class]: #compilerclass - -[api-compiler-function]: #compilerfunction - [api-data]: #data [api-freeze]: #processorfreeze [api-parser]: #parser -[api-parser-class]: #parserclass - -[api-parser-function]: #parserfunction - [api-pluggable]: #pluggable [api-pluggable-list]: #pluggablelist diff --git a/test/freeze.js b/test/freeze.js index a4097011..2fae2395 100644 --- a/test/freeze.js +++ b/test/freeze.js @@ -221,7 +221,7 @@ test('`freeze`', async function (t) { */ function parse() { // type-coverage:ignore-next-line -- something with TS being wrong. - this.Parser = simpleParser + this.parser = simpleParser } // `this` in JS is buggy in TS. @@ -230,5 +230,5 @@ function parse() { */ function compile() { // type-coverage:ignore-next-line -- something with TS being wrong. - this.Compiler = simpleCompiler + this.compiler = simpleCompiler } diff --git a/test/parse.js b/test/parse.js index 1d4dc5e6..9e969ad3 100644 --- a/test/parse.js +++ b/test/parse.js @@ -10,16 +10,16 @@ import {VFile} from 'vfile' test('`parse`', async function (t) { const givenNode = {type: 'alpha'} - await t.test('should throw without `Parser`', async function () { + await t.test('should throw without `parser`', async function () { assert.throws(function () { unified().parse('') - }, /Cannot `parse` without `Parser`/) + }, /Cannot `parse` without `parser`/) }) await t.test('should support a plain function', async function () { const processor = unified() - processor.Parser = function (doc, file) { + processor.parser = function (doc, file) { assert.equal(typeof doc, 'string') assert.ok(file instanceof VFile) assert.equal(arguments.length, 2) @@ -33,7 +33,7 @@ test('`parse`', async function (t) { const processor = unified() // Note: arrow function intended (which doesn’t have a prototype). - processor.Parser = (doc, file) => { + processor.parser = (doc, file) => { assert.equal(typeof doc, 'string') assert.ok(file instanceof VFile) return givenNode @@ -41,61 +41,4 @@ test('`parse`', async function (t) { assert.equal(processor.parse('charlie'), givenNode) }) - - await t.test('should support a class', async function () { - const processor = unified() - - processor.Parser = class { - /** - * @param {string} doc - * @param {VFile} file - */ - constructor(doc, file) { - assert.equal(typeof doc, 'string') - assert.ok(file instanceof VFile) - assert.equal(arguments.length, 2) - } - - /** - * @returns {Node} - */ - parse() { - assert.equal(arguments.length, 0) - return givenNode - } - } - - assert.equal(processor.parse('charlie'), givenNode) - }) - - await t.test( - 'should support a constructor w/ `parse` in prototype', - async function () { - const processor = unified() - - /** - * @constructor - * @param {string} doc - * @param {VFile} file - */ - function Parser(doc, file) { - assert.equal(typeof doc, 'string') - assert.ok(file instanceof VFile) - assert.equal(arguments.length, 2) - } - - /** - * @returns {Node} - */ - // type-coverage:ignore-next-line -- for some reason TS does understand `Parser.prototype`, but not `Compiler.prototype`. - Parser.prototype.parse = function () { - assert.equal(arguments.length, 0) - return givenNode - } - - processor.Parser = Parser - - assert.equal(processor.parse('charlie'), givenNode) - } - ) }) diff --git a/test/process-compilers.js b/test/process-compilers.js index 4234ac3c..0a4a8f7a 100644 --- a/test/process-compilers.js +++ b/test/process-compilers.js @@ -8,8 +8,8 @@ test('process (compilers)', async function (t) { const processor = unified() const result = 'bravo' - processor.Parser = simpleParser - processor.Compiler = function () { + processor.parser = simpleParser + processor.compiler = function () { return result } @@ -23,8 +23,8 @@ test('process (compilers)', async function (t) { const processor = unified() const result = new Uint8Array([0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63]) - processor.Parser = simpleParser - processor.Compiler = function () { + processor.parser = simpleParser + processor.compiler = function () { return result } @@ -44,10 +44,10 @@ test('process (compilers)', async function (t) { type: 'p' } - processor.Parser = simpleParser + processor.parser = simpleParser // @ts-expect-error: custom result, which should be registered! - processor.Compiler = function () { + processor.compiler = function () { return result } diff --git a/test/process-sync.js b/test/process-sync.js index 4184298d..6ddcf304 100644 --- a/test/process-sync.js +++ b/test/process-sync.js @@ -4,25 +4,25 @@ import {unified} from 'unified' import {simpleCompiler, simpleParser} from './util/simple.js' test('`processSync`', async function (t) { - await t.test('should throw w/o `Parser`', async function () { + await t.test('should throw w/o `parser`', async function () { assert.throws(function () { unified().processSync('') - }, /Cannot `processSync` without `Parser`/) + }, /Cannot `processSync` without `parser`/) }) - await t.test('should throw w/o `Compiler`', async function () { + await t.test('should throw w/o `compiler`', async function () { assert.throws(function () { const processor = unified() - processor.Parser = simpleParser + processor.parser = simpleParser processor.processSync('') - }, /Cannot `processSync` without `Compiler`/) + }, /Cannot `processSync` without `compiler`/) }) await t.test('should support `processSync`', async function () { const processor = unified() - processor.Parser = simpleParser - processor.Compiler = simpleCompiler + processor.parser = simpleParser + processor.compiler = simpleCompiler assert.equal(processor.processSync('alpha').toString(), 'alpha') }) @@ -32,8 +32,8 @@ test('`processSync`', async function (t) { async function () { assert.throws(function () { const processor = unified() - processor.Parser = simpleParser - processor.Compiler = simpleCompiler + processor.parser = simpleParser + processor.compiler = simpleCompiler processor .use(function () { @@ -65,7 +65,7 @@ test('`processSync`', async function (t) { */ function parse() { // type-coverage:ignore-next-line -- something with TS being wrong. - this.Parser = simpleParser + this.parser = simpleParser } // `this` in JS is buggy in TS. @@ -74,5 +74,5 @@ function parse() { */ function compile() { // type-coverage:ignore-next-line -- something with TS being wrong. - this.Compiler = simpleCompiler + this.compiler = simpleCompiler } diff --git a/test/process.js b/test/process.js index e2290fb6..ebae2b1d 100644 --- a/test/process.js +++ b/test/process.js @@ -8,31 +8,31 @@ test('`process`', async function (t) { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} - await t.test('should throw w/o `Parser`', async function () { + await t.test('should throw w/o `parser`', async function () { assert.throws(function () { unified().process('') - }, /Cannot `process` without `Parser`/) + }, /Cannot `process` without `parser`/) }) - await t.test('should throw w/o `Compiler`', async function () { + await t.test('should throw w/o `compiler`', async function () { assert.throws(function () { const processor = unified() - processor.Parser = simpleParser + processor.parser = simpleParser processor.process('') - }, /Cannot `process` without `Compiler`/) + }, /Cannot `process` without `compiler`/) }) await t.test('should pass/yield expected values', async function () { const processor = unified() - processor.Parser = function (doc, file) { + processor.parser = function (doc, file) { assert.equal(typeof doc, 'string') assert.equal(file, givenFile) assert.equal(arguments.length, 2) return givenNode } - processor.Compiler = function (tree, file) { + processor.compiler = function (tree, file) { assert.equal(tree, givenNode) assert.equal(file, givenFile) assert.equal(arguments.length, 2) @@ -59,8 +59,8 @@ test('`process`', async function (t) { await t.test('should rethrow errors in `done` throws', async function () { const processor = unified() - processor.Parser = simpleParser - processor.Compiler = simpleCompiler + processor.parser = simpleParser + processor.compiler = simpleCompiler assert.throws(function () { processor.process(givenFile, function () { @@ -74,8 +74,8 @@ test('`process`', async function (t) { async function () { const processor = unified() - processor.Parser = simpleParser - processor.Compiler = simpleCompiler + processor.parser = simpleParser + processor.compiler = simpleCompiler await new Promise(function (resolve, reject) { processor.process(givenFile).then( diff --git a/test/stringify.js b/test/stringify.js index bc55efee..d4a022c1 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -11,16 +11,16 @@ test('`stringify`', async function (t) { const givenFile = new VFile('alpha') const givenNode = {type: 'bravo'} - await t.test('should throw without `Compiler`', async function () { + await t.test('should throw without `compiler`', async function () { assert.throws(function () { unified().stringify(givenNode) - }, /Cannot `stringify` without `Compiler`/) + }, /Cannot `stringify` without `compiler`/) }) await t.test('should support a plain function', async function () { const processor = unified() - processor.Compiler = function (node, file) { + processor.compiler = function (node, file) { assert.equal(node, givenNode) assert.ok(file instanceof VFile) assert.equal(arguments.length, 2) @@ -34,7 +34,7 @@ test('`stringify`', async function (t) { const processor = unified() // Note: arrow function intended (which doesn’t have a prototype). - processor.Compiler = (node, file) => { + processor.compiler = (node, file) => { assert.equal(node, givenNode, 'should pass a node') assert.ok(file instanceof VFile, 'should pass a file') return 'echo' @@ -42,54 +42,4 @@ test('`stringify`', async function (t) { assert.equal(processor.stringify(givenNode, givenFile), 'echo') }) - - await t.test('should support a class', async function () { - const processor = unified() - - processor.Compiler = class { - /** - * @param {Node} node - * @param {VFile} file - */ - constructor(node, file) { - assert.equal(node, givenNode) - assert.ok(file instanceof VFile) - } - - compile() { - assert.equal(arguments.length, 0) - return 'echo' - } - } - - assert.equal(processor.stringify(givenNode, givenFile), 'echo') - }) - - await t.test( - 'should support a constructor w/ `compile` in prototype', - async function () { - const processor = unified() - - /** - * @constructor - * @param {Node} node - * @param {VFile} file - */ - function Compiler(node, file) { - assert.equal(node, givenNode, 'should pass a node') - assert.ok(file instanceof VFile, 'should pass a file') - assert.equal(arguments.length, 2) - } - - // type-coverage:ignore-next-line -- for some reason TS does understand `Compiler.prototype`, but not `Compiler.prototype`. - Compiler.prototype.compile = function () { - assert.equal(arguments.length, 0) - return 'echo' - } - - processor.Compiler = Compiler - - assert.equal(processor.stringify(givenNode, givenFile), 'echo') - } - ) }) From baf80b20d604c920a0656b4eec1c17612689f5ba Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 14 Aug 2023 20:08:04 +0200 Subject: [PATCH 43/46] Change to require Node.js 16 --- readme.md | 4 ++-- tsconfig.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 0fc7fe75..9cce5d71 100644 --- a/readme.md +++ b/readme.md @@ -1544,8 +1544,8 @@ versions of Node.js. When we cut a new major release, we drop support for unmaintained versions of Node. -This means we try to keep the current release line, `unified@^10`, compatible -with Node.js 12. +This means we try to keep the current release line, `unified@^11`, compatible +with Node.js 16. ## Contribute diff --git a/tsconfig.json b/tsconfig.json index 8e70d195..2c12ea63 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,10 +5,10 @@ "declaration": true, "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, - "lib": ["es2020"], + "lib": ["es2022"], "module": "node16", "strict": true, - "target": "es2020" + "target": "es2022" }, "exclude": ["coverage/", "node_modules/"], "include": ["**/*.js", "test/types.d.ts", "index.d.ts"] From afb704a79e18a5d98ca111ad1e62f6b8a5b2ee16 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Tue, 15 Aug 2023 01:50:29 -0700 Subject: [PATCH 44/46] Fix some typos Closes GH-225 Reviewed-by: Remco Haszing Reviewed-by: Titus Wormer --- readme.md | 2 +- script/fix-types.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 9cce5d71..034a504d 100644 --- a/readme.md +++ b/readme.md @@ -1089,7 +1089,7 @@ Plugins configure the processors they are applied on in the following ways: configuring data * they specify how to handle trees and files -In practise, they are functions that can receive options and configure the +In practice, they are functions that can receive options and configure the processor (`this`). > 👉 **Note**: plugins are called when the processor is *frozen*, not when they diff --git a/script/fix-types.js b/script/fix-types.js index 7495341f..905cfe8f 100644 --- a/script/fix-types.js +++ b/script/fix-types.js @@ -37,7 +37,7 @@ const result = file if (file === result) { console.error( - 'Could not fix `lib/index.d.ts`, was `tsc` fixed somewhow? Or were changes already applied?' + 'Could not fix `lib/index.d.ts`, was `tsc` fixed somehow? Or were changes already applied?' ) } else { await fs.writeFile(url, result) From d83cb8de776acad9c142209774338e1dbd8e36ea Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 15 Aug 2023 10:50:59 +0200 Subject: [PATCH 45/46] Fix another typo --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 0fde9644..e2a11971 100644 --- a/lib/index.js +++ b/lib/index.js @@ -128,7 +128,7 @@ * configuring data * * they specify how to handle trees and files * - * In practise, they are functions that can receive options and configure the + * In practice, they are functions that can receive options and configure the * processor (`this`). * * > 👉 **Note**: plugins are called when the processor is *frozen*, not when From 98ab67534dd4e07fe4032afc1154c1ee8c81dd10 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 15 Aug 2023 10:54:25 +0200 Subject: [PATCH 46/46] 11.0.0 --- package.json | 2 +- readme.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 235a4bb8..0ba6d461 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unified", - "version": "10.1.2", + "version": "11.0.0", "description": "parse, inspect, transform, and serialize content through syntax trees", "license": "MIT", "keywords": [ diff --git a/readme.md b/readme.md index 034a504d..ff81003c 100644 --- a/readme.md +++ b/readme.md @@ -104,14 +104,14 @@ npm install unified In Deno with [`esm.sh`][esmsh]: ```js -import {unified} from 'https://esm.sh/unified@10' +import {unified} from 'https://esm.sh/unified@11' ``` In browsers with [`esm.sh`][esmsh]: ```html ```