From 134ecadaa602b47e27bb405271284c828558a962 Mon Sep 17 00:00:00 2001 From: Titus Date: Fri, 30 Jul 2021 12:00:10 +0200 Subject: [PATCH] Add plugin input/output type parameters This commit adds support for tracking the parse tree, current tree, compile tree, and compile result as configured on unified processors. Plugins can now configure what input and output they receive and yield: ```ts // A parse plugin, that configures parser, defines what it receives as `string` // and what it yields as a specific node. const remarkParse: Plugin = () => {} // A transform plugin, that transforms a certain tree, defines what it receives // (and yields) as a specific node. const remarkTransformPlugin: Plugin = () => {} // A bridge plugin, that transforms a certain tree to another, defines what it // receives as a specific node and yields as another node. const remarkRehype: Plugin = () => {} // A compile plugin, that configures a compiler, defines what it receives as a // specific node and yields as a non-node value (typically string, but could be // a React node). const rehypeStringify: Plugin = () => {} ``` Assuming the above plugins are used: ```js const processor = unified() .use(remarkParse) .use(remarkTransformPlugin) .use(remarkRehype) .use(rehypeStringify) ``` Affects what the processor functions receive and yield: ```js import {expectType} from 'tsd' const tree: MdastRoot = {type: 'root', children: []} expectType(processor.parse('')) expectType(processor.stringify(tree)) expectType(processor.runSync(tree)) ``` Closes GH-156. Reviewed-by: Christian Murphy --- index.d.ts | 380 ++++++++++++++++++++++++++++++++++------- index.test-d.ts | 320 +++++++++++++++++++++++++++++++++- test/async-function.js | 19 ++- test/run.js | 6 + test/use.js | 16 +- 5 files changed, 663 insertions(+), 78 deletions(-) diff --git a/index.d.ts b/index.d.ts index 0739b57c..916901b5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,16 +16,104 @@ import {Node} from 'unist' import {VFile, VFileCompatible} from 'vfile' +/* eslint-disable @typescript-eslint/ban-types */ + +type VFileWithOutput = Result extends Uint8Array // Buffer. + ? 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 + +// Create a processor based on the input/output of a plugin. +type UsePlugin< + ParseTree extends Node | void = void, + CurrentTree extends Node | void = void, + CompileTree extends Node | void = void, + CompileResult = void, + Input = void, + Output = void +> = Output extends Node + ? Input extends string + ? // If `Input` is `string` and `Output` is `Node`, then this plugin + // defines a parser, so set `ParseTree`. + Processor< + Output, + Specific, + Specific, + CompileResult + > + : Input extends Node + ? // If `Input` is `Node` and `Output` is `Node`, then this plugin defines a + // transformer, its output defines the input of the next, so set + // `CurrentTree`. + Processor< + Specific, + Output, + Specific, + CompileResult + > + : // Else, `Input` is something else and `Output` is `Node`: + never + : Input extends Node + ? // If `Input` is `Node` and `Output` is not a `Node`, then this plugin + // defines a compiler, so set `CompileTree` and `CompileResult` + Processor< + Specific, + Specific, + Input, + Output + > + : // Else, `Input` is not a `Node` and `Output` is not a `Node`. + // Maybe it’s untyped, or the plugin throws an error (`never`), so lets + // just keep it as it was. + Processor + +/* eslint-enable @typescript-eslint/ban-types */ + /** * Processor allows plugins to be chained together to transform content. * The chain of plugins defines how content flows through it. + * + * @typeParam ParseTree + * The node that the parser yields (and `run` receives). + * @typeParam CurrentTree + * The node that the last attached plugin yields. + * @typeParam CompileTree + * The node that the compiler receives (and `run` yields). + * @typeParam CompileResult + * The thing that the compiler yields. */ -export interface Processor extends FrozenProcessor { +export interface Processor< + ParseTree extends Node | void = void, + CurrentTree extends Node | void = void, + CompileTree extends Node | void = void, + CompileResult = void +> extends FrozenProcessor { /** * Configure the processor to use a plugin. * * @typeParam PluginParameters * Plugin settings. + * @typeParam Input + * Value that is accepted by the plugin. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer expects. + * * If the plugin sets a parser, then this should be `string`. + * * If the plugin sets a compiler, then this should be the node type that + * the compiler expects. + * @typeParam Output + * Value that the plugin yields. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer yields, and defaults to `Input`. + * * If the plugin sets a parser, then this should be the node type that + * the parser yields. + * * If the plugin sets a compiler, then this should be the result that + * the compiler yields (`string`, `Buffer`, or something else). * @param plugin * Plugin (function) to use. * Plugins are deduped based on identity: passing a function in twice will @@ -39,16 +127,44 @@ export interface Processor extends FrozenProcessor { * @returns * Current processor. */ - use( - plugin: Plugin, + use< + PluginParameters extends any[] = any[], + Input = Specific, + Output = Input + >( + plugin: Plugin, ...settings: PluginParameters | [boolean] - ): Processor + ): UsePlugin< + ParseTree, + CurrentTree, + CompileTree, + CompileResult, + Input, + Output + > /** * Configure the processor with a tuple of a plugin and setting(s). * * @typeParam PluginParameters * Plugin settings. + * @typeParam Input + * Value that is accepted by the plugin. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer expects. + * * If the plugin sets a parser, then this should be `string`. + * * If the plugin sets a compiler, then this should be the node type that + * the compiler expects. + * @typeParam Output + * Value that the plugin yields. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer yields, and defaults to `Input`. + * * If the plugin sets a parser, then this should be the node type that + * the parser yields. + * * If the plugin sets a compiler, then this should be the result that + * the compiler yields (`string`, `Buffer`, or something else). * @param tuple * A tuple where the first item is a plugin (function) to use and other * items are options. @@ -59,9 +175,22 @@ export interface Processor extends FrozenProcessor { * @returns * Current processor. */ - use( - tuple: PluginTuple | [Plugin, boolean] - ): Processor + use< + PluginParameters extends any[] = any[], + Input = Specific, + Output = Input + >( + tuple: + | PluginTuple + | [Plugin, boolean] + ): UsePlugin< + ParseTree, + CurrentTree, + CompileTree, + CompileResult, + Input, + Output + > /** * Configure the processor with a preset or list of plugins and presets. @@ -73,7 +202,9 @@ export interface Processor extends FrozenProcessor { * @returns * Current processor. */ - use(presetOrList: Preset | PluggableList): Processor + use( + presetOrList: Preset | PluggableList + ): Processor } /** @@ -82,7 +213,12 @@ export interface Processor extends FrozenProcessor { * A frozen processor can be created by calling `.freeze()` on a processor. * An unfrozen processor can be created by calling a processor. */ -export interface FrozenProcessor { +export interface FrozenProcessor< + ParseTree extends Node | void = void, + CurrentTree extends Node | void = void, + CompileTree extends Node | void = void, + CompileResult = void +> { /** * Clone current processor * @@ -92,7 +228,7 @@ export interface FrozenProcessor { * But when the descendant processor is configured it does not affect the * ancestral processor. */ - (): Processor + (): Processor /** * Internal list of configured plugins. @@ -101,8 +237,10 @@ export interface FrozenProcessor { */ attachers: Array<[Plugin, ...unknown[]]> - Parser?: Parser | undefined - Compiler?: Compiler | undefined + Parser?: Parser> | undefined + Compiler?: + | Compiler, Specific> + | undefined /** * Parse a file. @@ -113,7 +251,7 @@ export interface FrozenProcessor { * @returns * Resulting tree. */ - parse(file?: VFileCompatible | undefined): Node + parse(file?: VFileCompatible | undefined): Specific /** * Compile a file. @@ -127,7 +265,10 @@ export interface FrozenProcessor { * This depends on which plugins you use: typically text, but could for * example be a React node. */ - stringify(node: Node, file?: VFileCompatible | undefined): unknown + stringify( + node: Specific, + file?: VFileCompatible | undefined + ): CompileTree extends Node ? CompileResult : unknown /** * Run transforms on the given tree. @@ -139,7 +280,10 @@ export interface FrozenProcessor { * @returns * Nothing. */ - run(node: Node, callback: RunCallback): void + run( + node: Specific, + callback: RunCallback> + ): void /** * Run transforms on the given node. @@ -155,9 +299,9 @@ export interface FrozenProcessor { * Nothing. */ run( - node: Node, + node: Specific, file: VFileCompatible | undefined, - callback: RunCallback + callback: RunCallback> ): void /** @@ -171,10 +315,13 @@ export interface FrozenProcessor { * @returns * Promise that resolves to the resulting tree. */ - run(node: Node, file?: VFileCompatible | undefined): Promise + run( + node: Specific, + file?: VFileCompatible | undefined + ): Promise> /** - * Run transforms on the given node, synchroneously. + * Run transforms on the given node, synchronously. * Throws when asynchronous transforms are configured. * * @param node @@ -185,7 +332,10 @@ export interface FrozenProcessor { * @returns * Resulting tree. */ - runSync(node: Node, file?: VFileCompatible | undefined): Node + runSync( + node: Specific, + file?: VFileCompatible | undefined + ): Specific /** * Process a file. @@ -210,7 +360,10 @@ export interface FrozenProcessor { * @returns * Nothing. */ - process(file: VFileCompatible | undefined, callback: ProcessCallback): void + process( + file: VFileCompatible | undefined, + callback: ProcessCallback> + ): void /** * Process a file. @@ -233,10 +386,10 @@ export interface FrozenProcessor { * @returns * Promise that resolves to the resulting `VFile`. */ - process(file: VFileCompatible): Promise + process(file: VFileCompatible): Promise> /** - * Process a file, synchroneously. + * Process a file, synchronously. * Throws when asynchronous transforms are configured. * * This performs all phases of the processor: @@ -257,7 +410,9 @@ export interface FrozenProcessor { * @returns * Resulting file. */ - processSync(file?: VFileCompatible | undefined): VFile + processSync( + file?: VFileCompatible | undefined + ): VFileWithOutput /** * Get an in-memory key-value store accessible to all phases of the process. @@ -275,7 +430,9 @@ export interface FrozenProcessor { * @returns * Current processor. */ - data(data: Record): Processor + data( + data: Record + ): Processor /** * Get an in-memory value by key. @@ -297,7 +454,10 @@ export interface FrozenProcessor { * @returns * Current processor. */ - data(key: string, value: unknown): Processor + data( + key: string, + value: unknown + ): Processor /** * Freeze a processor. @@ -315,7 +475,7 @@ export interface FrozenProcessor { * @returns * Frozen processor. */ - freeze(): FrozenProcessor + freeze(): FrozenProcessor } /** @@ -327,6 +487,23 @@ export interface FrozenProcessor { * * @typeParam PluginParameters * Plugin settings. + * @typeParam Input + * Value that is accepted by the plugin. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer expects. + * * If the plugin sets a parser, then this should be `string`. + * * If the plugin sets a compiler, then this should be the node type that + * the compiler expects. + * @typeParam Output + * Value that the plugin yields. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer yields, and defaults to `Input`. + * * If the plugin sets a parser, then this should be the node type that + * the parser yields. + * * If the plugin sets a compiler, then this should be the result that + * the compiler yields (`string`, `Buffer`, or something else). * @this * The current processor. * Plugins can configure the processor by interacting with `this.Parser` or @@ -347,10 +524,29 @@ export interface FrozenProcessor { * Plugins can return a `Transformer` to specify how the syntax tree is * handled. */ -export type Plugin = ( - this: Processor, +export type Plugin< + PluginParameters extends any[] = any[], + Input = Node, + Output = Input +> = ( + this: Input extends Node + ? Output extends Node + ? // This is a transform, so define `Input` as the current tree. + Processor + : // Compiler. + Processor + : Output extends Node + ? // Parser. + Processor + : // No clue. + Processor, ...settings: PluginParameters -) => Transformer | void +) => // If both `Input` and `Output` are `Node`, expect an optional `Transformer`. +Input extends Node + ? Output extends Node + ? Transformer | void + : void + : void /** * Presets provide a sharable way to configure processors with multiple plugins @@ -369,11 +565,29 @@ export interface Preset { * * @typeParam PluginParameters * Plugin settings. + * @typeParam Input + * Value that is accepted by the plugin. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer expects. + * * If the plugin sets a parser, then this should be `string`. + * * If the plugin sets a compiler, then this should be the node type that + * the compiler expects. + * @typeParam Output + * Value that the plugin yields. + * + * * If the plugin returns a transformer, then this should be the node + * type that the transformer yields, and defaults to `Input`. + * * If the plugin sets a parser, then this should be the node type that + * the parser yields. + * * If the plugin sets a compiler, then this should be the result that + * the compiler yields (`string`, `Buffer`, or something else). */ -export type PluginTuple = [ - Plugin, - ...PluginParameters -] +export type PluginTuple< + PluginParameters extends any[] = any[], + Input = Node, + Output = Input +> = [Plugin, ...PluginParameters] /** * A union of the different ways to add plugins and settings. @@ -382,8 +596,8 @@ export type PluginTuple = [ * Plugin settings. */ export type Pluggable = - | PluginTuple - | Plugin + | PluginTuple + | Plugin | Preset /** @@ -395,8 +609,11 @@ export type PluggableList = Pluggable[] * @deprecated * Please use `Plugin`. */ -export type Attacher = - Plugin +export type Attacher< + PluginParameters extends any[] = any[], + Input = Node, + Output = Input +> = Plugin /** * Transformers modify the syntax tree or metadata of a file. @@ -405,6 +622,10 @@ export type Attacher = * If an error occurs (either because it’s thrown, returned, rejected, or passed * to `next`), the process stops. * + * @typeParam Input + * Node type that the transformer expects. + * @typeParam Output + * Node type that the transformer yields. * @param node * Tree to be transformed. * @param file @@ -428,15 +649,20 @@ export type Attacher = * * If you accept a `next` callback, nothing should be returned. */ -type Transformer = ( - node: Node, +export type Transformer< + Input extends Node = Node, + Output extends Node = Input +> = ( + node: Input, file: VFile, - next: TransformCallback -) => Promise | Node | Error | undefined | void + next: TransformCallback +) => Promise | Output | Error | undefined | void /** * Callback you must call when a transformer is done. * + * @typeParam Tree + * Node that the plugin yields. * @param error * Pass an error to stop the process. * @param node @@ -446,9 +672,9 @@ type Transformer = ( * @returns * Nothing. */ -export type TransformCallback = ( +export type TransformCallback = ( error?: Error | null | undefined, - node?: Node | undefined, + node?: Tree | undefined, file?: VFile | undefined ) => void @@ -464,13 +690,21 @@ export type TransformCallback = ( * `prototype`), in which case it’s called with `new`. * Instances must have a parse method that is called without arguments and * must return a `Node`. + * + * @typeParam Tree + * The node that the parser yields (and `run` receives). */ -export type Parser = ParserClass | ParserFunction +export type Parser = + | ParserClass + | ParserFunction /** * A class to parse files. + * + * @typeParam Tree + * The node that the parser yields. */ -export class ParserClass { +export class ParserClass { prototype: { /** * Parse a file. @@ -478,7 +712,7 @@ export class ParserClass { * @returns * Parsed tree. */ - parse(): Node + parse(): Tree } /** @@ -497,6 +731,8 @@ export class ParserClass { /** * Normal function to parse a file. * + * @typeParam Tree + * The node that the parser yields. * @param document * Document to parse. * @param file @@ -504,7 +740,10 @@ export class ParserClass { * @returns * Node representing the given file. */ -export type ParserFunction = (document: string, file: VFile) => Node +export type ParserFunction = ( + document: string, + file: VFile +) => Tree /** * Function handling the compilation of syntax tree to a text. @@ -518,13 +757,25 @@ export type ParserFunction = (document: string, file: VFile) => Node * `prototype`), in which case it’s called with `new`. * Instances must have a `compile` method that is called without arguments * and must return a `string`. + * + * @typeParam Tree + * The node that the compiler receives. + * @typeParam Result + * The thing that the compiler yields. */ -export type Compiler = CompilerClass | CompilerFunction +export type Compiler = + | CompilerClass + | CompilerFunction /** * A class to compile trees. + * + * @typeParam Tree + * The node that the compiler receives. + * @typeParam Result + * The thing that the compiler yields. */ -export class CompilerClass { +export class CompilerClass { prototype: { /** * Compile a tree. @@ -533,7 +784,7 @@ export class CompilerClass { * New content: compiled text (`string` or `Buffer`, for `file.value`) or * something else (for `file.result`). */ - compile(): unknown + compile(): Result } /** @@ -546,12 +797,16 @@ export class CompilerClass { * @returns * Instance. */ - constructor(tree: Node, file: VFile) + constructor(tree: Tree, file: VFile) } /** * Normal function to compile a tree. * + * @typeParam Tree + * The node that the compiler receives. + * @typeParam Result + * The thing that the compiler yields. * @param tree * Tree to compile. * @param file @@ -560,13 +815,18 @@ export class CompilerClass { * New content: compiled text (`string` or `Buffer`, for `file.value`) or * something else (for `file.result`). */ -export type CompilerFunction = (tree: Node, file: VFile) => unknown +export type CompilerFunction = ( + tree: Tree, + file: VFile +) => Result /** * Callback called when a done running. * + * @typeParam Tree + * The tree that the callback receives. * @param error - * Error passed when unsuccesful. + * Error passed when unsuccessful. * @param node * Tree to transform. * @param file @@ -574,25 +834,27 @@ export type CompilerFunction = (tree: Node, file: VFile) => unknown * @returns * Nothing. */ -export type RunCallback = ( +export type RunCallback = ( error?: Error | null | undefined, - node?: Node | undefined, + node?: Tree | undefined, file?: VFile | undefined ) => void /** * Callback called when a done processing. * + * @typeParam File + * The file that the callback receives. * @param error - * Error passed when unsuccesful. + * Error passed when unsuccessful. * @param file * File passed when successful. * @returns * Nothing. */ -export type ProcessCallback = ( +export type ProcessCallback = ( error?: Error | null | undefined, - file?: VFile | undefined + file?: File | undefined ) => void /** diff --git a/index.test-d.ts b/index.test-d.ts index 1572d157..5ff91e09 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,5 +1,7 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ + import {expectType, expectError} from 'tsd' -import {Node} from 'unist' +import {Node, Parent, Literal} from 'unist' import {VFile} from 'vfile' import { unified, @@ -155,6 +157,318 @@ unified() .use(() => () => { /* Empty */ }) - .use(() => () => { - throw new Error('x') + .use(() => (x) => { + if (x) { + throw new Error('x') + } }) + +// Plugins bound to a certain node. + +// A small subset of mdast. +interface MdastRoot extends Parent { + type: 'root' + children: MdastFlow[] +} + +type MdastFlow = MdastParagraph + +interface MdastParagraph extends Parent { + type: 'paragraph' + children: MdastPhrasing[] +} + +type MdastPhrasing = MdastText + +interface MdastText extends Literal { + type: 'text' + value: string +} + +// A small subset of hast. +interface HastRoot extends Parent { + type: 'root' + children: HastChild[] +} + +type HastChild = HastElement | HastText + +interface HastElement extends Parent { + type: 'element' + tagName: string + properties: Record + children: HastChild[] +} + +interface HastText extends Literal { + type: 'text' + value: string +} + +const explicitPluginWithInputTree: Plugin = + () => (tree, file) => { + expectType(tree) + expectType(file) + } + +const explicitPluginWithTrees: Plugin = + () => (tree, file) => { + expectType(tree) + expectType(file) + return { + type: 'root', + children: [ + { + type: 'element', + tagName: 'a', + properties: {}, + children: [{type: 'text', value: 'a'}] + } + ] + } + } + +unified().use(explicitPluginWithInputTree) +unified().use([explicitPluginWithInputTree]) +unified().use({plugins: [explicitPluginWithInputTree], settings: {}}) +unified().use(() => (tree: MdastRoot) => { + expectType(tree) +}) +unified().use([ + () => (tree: MdastRoot) => { + expectType(tree) + } +]) +unified().use({ + plugins: [ + () => (tree: MdastRoot) => { + 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. +interface ReactNode { + kind: string +} + +const someMdast: MdastRoot = { + type: 'root', + children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}] +} + +const someHast: HastRoot = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'a', + properties: {}, + children: [{type: 'text', value: 'a'}] + } + ] +} + +const remarkParse: Plugin = () => { + /* Empty */ +} + +const remarkStringify: Plugin = () => { + /* Empty */ +} + +const rehypeParse: Plugin = () => { + /* Empty */ +} + +const rehypeStringify: Plugin = () => { + /* Empty */ +} + +const rehypeStringifyBuffer: Plugin = () => { + /* Empty */ +} + +const explicitRemarkPlugin: Plugin = () => { + /* Empty */ +} + +const implicitPlugin: Plugin = () => { + /* Empty */ +} + +const remarkRehype: Plugin = () => { + /* Empty */ +} + +const explicitRehypePlugin: Plugin = () => { + /* Empty */ +} + +const rehypeReact: Plugin = () => { + /* Empty */ +} + +// 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('') +) + +// A parser plugin defines the input of `.run`: +expectType(unified().use(remarkParse).runSync(someMdast)) +expectError(unified().use(remarkParse).runSync(someHast)) + +// A compiler plugin defines the input/output of `.run`: +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).runSync(someMdast) +) +// Probably hast expected. +expectError(unified().use(rehypeStringify).runSync(someMdast)) + +unified() + .use(rehypeStringify) + .run(someHast) + .then((thing) => { + expectType(thing) + }) + +unified() + .use(rehypeStringify) + .run(someHast, (error, thing) => { + expectType(error) + expectType(thing) + }) + +// A compiler plugin defines the output of `.process`: +expectType( + unified().use(rehypeReact).processSync('') +) +expectType( + unified().use(remarkParse).use(rehypeReact).processSync('') +) + +unified() + .use(rehypeReact) + .process('') + .then((file) => { + expectType(file) + }) + +unified() + .use(rehypeReact) + .process('', (error, thing) => { + expectType(error) + expectType<(VFile & {result: ReactNode}) | undefined>(thing) + }) + +// Plugins work! +unified() + .use(remarkParse) + .use(explicitRemarkPlugin) + .use(implicitPlugin) + .use(remarkRehype) + .use(implicitPlugin) + .use(rehypeStringify) + .freeze() + +// 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) + }) + +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) + +// 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)) + +// 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) +) + +/* eslint-enable @typescript-eslint/no-floating-promises */ diff --git a/test/async-function.js b/test/async-function.js index cc6d2060..7f876e28 100644 --- a/test/async-function.js +++ b/test/async-function.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('unist').Node} Node + */ + import test from 'tape' import {VFile} from 'vfile' import {unified} from '../index.js' @@ -12,19 +16,18 @@ test('async function transformer () {}', (t) => { unified() .use(() => async function () {}) .use( + // Note: TS JS doesn’t understand the `Promise` w/o explicit type. + /** @type {import('../index.js').Plugin<[]>} */ () => async function () { return undefined } ) - .use( - () => - async function (tree, file) { - t.equal(tree, givenNode, 'passes correct tree to an async function') - t.equal(file, givenFile, 'passes correct file to an async function') - return modifiedNode - } - ) + .use(() => async (tree, file) => { + t.equal(tree, givenNode, 'passes correct tree to an async function') + t.equal(file, givenFile, 'passes correct file to an async function') + return modifiedNode + }) .run(givenNode, givenFile, (error, tree, file) => { t.error(error, 'should’t fail') t.equal(tree, modifiedNode, 'passes given tree to `done`') diff --git a/test/run.js b/test/run.js index 7ca0e300..c843b24b 100644 --- a/test/run.js +++ b/test/run.js @@ -118,6 +118,8 @@ test('run(node[, file], done)', (t) => { unified() .use( + // Note: TS JS doesn’t understand the promise w/o explicit type. + /** @type {import('../index.js').Plugin<[]>} */ () => function () { return new Promise((resolve) => { @@ -371,6 +373,8 @@ test('run(node[, file])', (t) => { unified() .use( + // Note: TS JS doesn’t understand the promise w/o explicit type. + /** @type {import('../index.js').Plugin<[]>} */ () => function () { return new Promise((resolve) => { @@ -579,6 +583,8 @@ test('runSync(node[, file])', (t) => { () => { unified() .use( + // Note: TS JS doesn’t understand the promise w/o explicit type. + /** @type {import('../index.js').Plugin<[]>} */ () => function () { return new Promise((resolve) => { diff --git a/test/use.js b/test/use.js index 43911c02..1a3eb423 100644 --- a/test/use.js +++ b/test/use.js @@ -262,19 +262,19 @@ test('use(plugin[, options])', (t) => { t.test('should attach transformers', (t) => { const processor = unified() const givenNode = {type: 'test'} + const condition = true t.plan(3) processor - .use( - () => - function (node, file) { - t.equal(node, givenNode, 'should attach a transformer (#1)') - t.ok('message' in file, 'should attach a transformer (#2)') + .use(() => (node, file) => { + t.equal(node, givenNode, 'should attach a transformer (#1)') + t.ok('message' in file, 'should attach a transformer (#2)') - throw new Error('Alpha bravo charlie') - } - ) + if (condition) { + throw new Error('Alpha bravo charlie') + } + }) .freeze() t.throws(