From ad06700216d5b02a2d00c13b85900228c2b36eff Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Thu, 10 Aug 2023 16:38:01 +0200 Subject: [PATCH] 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 */