remark-usage
Advanced tools
Comparing version 10.0.1 to 11.0.0
@@ -1,3 +0,2 @@ | ||
export default remarkUsage | ||
export type Options = import('./lib/index.js').Options | ||
import remarkUsage from './lib/index.js' | ||
export { default } from "./lib/index.js"; | ||
export type Options = import('./lib/index.js').Options; |
@@ -5,4 +5,2 @@ /** | ||
import remarkUsage from './lib/index.js' | ||
export default remarkUsage | ||
export {default} from './lib/index.js' |
/** | ||
* Plugin to add a usage example to a readme. | ||
* Add a usage example to a readme. | ||
* | ||
* @type {import('unified').Plugin<[Options?]|void[], Root>} | ||
* Looks for the first heading matching `options.heading` (case insensitive), | ||
* removes everything between it and an equal or higher next heading, and replaces | ||
* that with a example. | ||
* | ||
* The example runs in Node.js (so no side effects!). | ||
* Line comments (`//`) are turned into markdown. | ||
* Calls to `console.log()` are exposed as code blocks, containing the logged | ||
* values, so `console.log(1 + 1)` becomes `2`. | ||
* Use a string as the first argument to `log` to use as the language for the code. | ||
* | ||
* You can ignore lines with `remark-usage-ignore-next`: | ||
* | ||
* ```js | ||
* // remark-usage-ignore-next | ||
* const two = sum(1, 1) | ||
* | ||
* // remark-usage-ignore-next 3 | ||
* function sum(a, b) { | ||
* return a + b | ||
* } | ||
* ``` | ||
* | ||
* …if no `skip` is given, 1 line is skipped. | ||
* | ||
* @param {Readonly<Options> | null | undefined} [options] | ||
* Configuration (optional). | ||
* @returns | ||
* Transform. | ||
*/ | ||
export default function remarkUsage( | ||
options?: void | Options | undefined | ||
): | ||
| void | ||
| import('unified').Transformer<import('mdast').Root, import('mdast').Root> | ||
export type Root = import('mdast').Root | ||
export type BlockContent = import('mdast').BlockContent | ||
export type VFile = import('vfile').VFile | ||
export type Callback = import('trough').Callback | ||
export default function remarkUsage(options?: Readonly<Options> | null | undefined): (tree: Root, file: VFile) => Promise<undefined>; | ||
export type FileResult = import('@babel/core').BabelFileResult; | ||
export type NodePathCallExpression = import('@babel/core').NodePath<CallExpression>; | ||
export type NodePathImportDeclaration = import('@babel/core').NodePath<ImportDeclaration>; | ||
export type PluginPass = import('@babel/core').PluginPass; | ||
export type CallExpression = import('@babel/types').CallExpression; | ||
export type ImportDeclaration = import('@babel/types').ImportDeclaration; | ||
export type StringLiteral = import('@babel/types').StringLiteral; | ||
export type Root = import('mdast').Root; | ||
export type RootContent = import('mdast').RootContent; | ||
export type PackageJson = import('type-fest').PackageJson; | ||
/** | ||
* Code. | ||
*/ | ||
export type Code = { | ||
/** | ||
* End. | ||
*/ | ||
end: number; | ||
/** | ||
* Start. | ||
*/ | ||
start: number; | ||
/** | ||
* Type. | ||
*/ | ||
type: 'code'; | ||
}; | ||
/** | ||
* Code. | ||
*/ | ||
export type CodeWithValues = { | ||
/** | ||
* Kind. | ||
*/ | ||
type: 'code'; | ||
/** | ||
* Results. | ||
*/ | ||
values: Array<string>; | ||
}; | ||
/** | ||
* Comment. | ||
*/ | ||
export type Comment = { | ||
/** | ||
* Type. | ||
*/ | ||
type: 'comment'; | ||
/** | ||
* Values. | ||
*/ | ||
values: Array<string>; | ||
}; | ||
export type GenerateOptions = { | ||
example: string; | ||
name: string | null | undefined; | ||
instrumented: InstrumentResult; | ||
}; | ||
/** | ||
* Configuration. | ||
*/ | ||
export type InstrumentOptions = { | ||
/** | ||
* File. | ||
*/ | ||
example: VFile; | ||
/** | ||
* ID. | ||
*/ | ||
id: string; | ||
/** | ||
* Main. | ||
*/ | ||
main?: string | null | undefined; | ||
}; | ||
export type InstrumentResult = { | ||
result: string; | ||
logs: Array<Log>; | ||
references: Array<Reference>; | ||
}; | ||
/** | ||
* Log. | ||
*/ | ||
export type Log = { | ||
/** | ||
* End. | ||
*/ | ||
end: number | null | undefined; | ||
/** | ||
* Code language. | ||
*/ | ||
language: string | undefined; | ||
/** | ||
* Start. | ||
*/ | ||
start: number | null | undefined; | ||
/** | ||
* Values. | ||
*/ | ||
values: Array<string>; | ||
}; | ||
/** | ||
* Info on the package. | ||
*/ | ||
export type PackageInfo = { | ||
/** | ||
* Data. | ||
*/ | ||
value: PackageJson; | ||
/** | ||
* File. | ||
*/ | ||
file: VFile; | ||
}; | ||
/** | ||
* Reference. | ||
*/ | ||
export type Reference = { | ||
/** | ||
* End. | ||
*/ | ||
end: number | null | undefined; | ||
/** | ||
* Quote. | ||
*/ | ||
quote: string; | ||
/** | ||
* Start. | ||
*/ | ||
start: number | null | undefined; | ||
}; | ||
/** | ||
* Skip. | ||
*/ | ||
export type Skip = { | ||
/** | ||
* Skip. | ||
*/ | ||
skip: number; | ||
/** | ||
* Type. | ||
*/ | ||
type: 'skip'; | ||
}; | ||
/** | ||
* Token. | ||
*/ | ||
export type Token = Code | Comment | Skip; | ||
/** | ||
* Configuration. | ||
*/ | ||
export type Options = { | ||
/** | ||
* Heading to look for (default: `'usage'`). | ||
* Wrapped in `new RegExp('^(' + value + ')$', 'i');`. | ||
*/ | ||
heading?: string | undefined | ||
/** | ||
* Path to the example script. | ||
* If given, resolved from `file.cwd`. | ||
* If not given, the following values are attempted and resolved from | ||
* `file.cwd`: | ||
* | ||
* * `'./example.js'` | ||
* * `'./example/index.js'` | ||
* * `'./examples.js'` | ||
* * `'./examples/index.js'` | ||
* * `'./doc/example.js'` | ||
* * `'./doc/example/index.js'` | ||
* * `'./docs/example.js'` | ||
* * `'./docs/example/index.js'` | ||
* | ||
* The first that exists, is used. | ||
*/ | ||
example?: string | undefined | ||
/** | ||
* Name of the module (default: `pkg.name`, optional). | ||
* Used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
*/ | ||
name?: string | undefined | ||
/** | ||
* Path to the main file (default: `pkg.main` or `'./index.js'`, optional). | ||
* If given, resolved from `file.cwd`. | ||
* If inferred from `package.json`, resolved relating to that package root. | ||
* Used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
*/ | ||
main?: string | undefined | ||
} | ||
/** | ||
* Path to example file (optional); | ||
* resolved from `file.cwd`; | ||
* defaults to the first example that exists: | ||
* | ||
* * `'example.js'` | ||
* * `'example/index.js'` | ||
* * `'examples.js'` | ||
* * `'examples/index.js'` | ||
* * `'doc/example.js'` | ||
* * `'doc/example/index.js'` | ||
* * `'docs/example.js'` | ||
* * `'docs/example/index.js'` | ||
*/ | ||
example?: string | null | undefined; | ||
/** | ||
* Heading to look for (default: `'usage'`); | ||
* wrapped in `new RegExp('^(' + value + ')$', 'i');`. | ||
*/ | ||
heading?: string | null | undefined; | ||
/** | ||
* Path to the file (default: `pkg.exports`, `pkg.main`, `'index.js'`); | ||
* resolved from `file.cwd`; | ||
* used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
*/ | ||
main?: string | null | undefined; | ||
/** | ||
* Name of the module (default: `pkg.name`); | ||
* used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
*/ | ||
name?: string | null | undefined; | ||
}; | ||
import { VFile } from 'vfile'; |
809
lib/index.js
/** | ||
* @typedef {import('@babel/core').BabelFileResult} FileResult | ||
* @typedef {import('@babel/core').NodePath<CallExpression>} NodePathCallExpression | ||
* @typedef {import('@babel/core').NodePath<ImportDeclaration>} NodePathImportDeclaration | ||
* @typedef {import('@babel/core').PluginPass} PluginPass | ||
* @typedef {import('@babel/types').CallExpression} CallExpression | ||
* @typedef {import('@babel/types').ImportDeclaration} ImportDeclaration | ||
* @typedef {import('@babel/types').StringLiteral} StringLiteral | ||
* @typedef {import('mdast').Root} Root | ||
* @typedef {import('mdast').BlockContent} BlockContent | ||
* @typedef {import('vfile').VFile} VFile | ||
* @typedef {import('trough').Callback} Callback | ||
* @typedef {import('mdast').RootContent} RootContent | ||
* @typedef {import('type-fest').PackageJson} PackageJson | ||
*/ | ||
/** | ||
* @typedef Code | ||
* Code. | ||
* @property {number} end | ||
* End. | ||
* @property {number} start | ||
* Start. | ||
* @property {'code'} type | ||
* Type. | ||
* | ||
* @typedef Options | ||
* @typedef CodeWithValues | ||
* Code. | ||
* @property {'code'} type | ||
* Kind. | ||
* @property {Array<string>} values | ||
* Results. | ||
* | ||
* @typedef Comment | ||
* Comment. | ||
* @property {'comment'} type | ||
* Type. | ||
* @property {Array<string>} values | ||
* Values. | ||
* | ||
* @typedef GenerateOptions | ||
* @property {string} example | ||
* @property {string | null | undefined} name | ||
* @property {InstrumentResult} instrumented | ||
* | ||
* @typedef InstrumentOptions | ||
* Configuration. | ||
* @property {string} [heading] | ||
* Heading to look for (default: `'usage'`). | ||
* Wrapped in `new RegExp('^(' + value + ')$', 'i');`. | ||
* @property {string} [example] | ||
* Path to the example script. | ||
* If given, resolved from `file.cwd`. | ||
* If not given, the following values are attempted and resolved from | ||
* `file.cwd`: | ||
* @property {VFile} example | ||
* File. | ||
* @property {string} id | ||
* ID. | ||
* @property {string | null | undefined} [main] | ||
* Main. | ||
* | ||
* * `'./example.js'` | ||
* * `'./example/index.js'` | ||
* * `'./examples.js'` | ||
* * `'./examples/index.js'` | ||
* * `'./doc/example.js'` | ||
* * `'./doc/example/index.js'` | ||
* * `'./docs/example.js'` | ||
* * `'./docs/example/index.js'` | ||
* @typedef InstrumentResult | ||
* @property {string} result | ||
* @property {Array<Log>} logs | ||
* @property {Array<Reference>} references | ||
* | ||
* The first that exists, is used. | ||
* @property {string} [name] | ||
* Name of the module (default: `pkg.name`, optional). | ||
* Used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
* @property {string} [main] | ||
* Path to the main file (default: `pkg.main` or `'./index.js'`, optional). | ||
* If given, resolved from `file.cwd`. | ||
* If inferred from `package.json`, resolved relating to that package root. | ||
* Used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
* @typedef Log | ||
* Log. | ||
* @property {number | null | undefined} end | ||
* End. | ||
* @property {string | undefined} language | ||
* Code language. | ||
* @property {number | null | undefined} start | ||
* Start. | ||
* @property {Array<string>} values | ||
* Values. | ||
* | ||
* @typedef PackageInfo | ||
* Info on the package. | ||
* @property {PackageJson} value | ||
* Data. | ||
* @property {VFile} file | ||
* File. | ||
* | ||
* @typedef Reference | ||
* Reference. | ||
* @property {number | null | undefined} end | ||
* End. | ||
* @property {string} quote | ||
* Quote. | ||
* @property {number | null | undefined} start | ||
* Start. | ||
* | ||
* @typedef Skip | ||
* Skip. | ||
* @property {number} skip | ||
* Skip. | ||
* @property {'skip'} type | ||
* Type. | ||
* | ||
* @typedef {Code | Comment | Skip} Token | ||
* Token. | ||
*/ | ||
import fs from 'node:fs' | ||
/** | ||
* @typedef Options | ||
* Configuration. | ||
* @property {string | null | undefined} [example] | ||
* Path to example file (optional); | ||
* resolved from `file.cwd`; | ||
* defaults to the first example that exists: | ||
* | ||
* * `'example.js'` | ||
* * `'example/index.js'` | ||
* * `'examples.js'` | ||
* * `'examples/index.js'` | ||
* * `'doc/example.js'` | ||
* * `'doc/example/index.js'` | ||
* * `'docs/example.js'` | ||
* * `'docs/example/index.js'` | ||
* @property {string | null | undefined} [heading] | ||
* Heading to look for (default: `'usage'`); | ||
* wrapped in `new RegExp('^(' + value + ')$', 'i');`. | ||
* @property {string | null | undefined} [main] | ||
* Path to the file (default: `pkg.exports`, `pkg.main`, `'index.js'`); | ||
* resolved from `file.cwd`; | ||
* used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
* @property {string | null | undefined} [name] | ||
* Name of the module (default: `pkg.name`); | ||
* used to rewrite `import x from './main.js'` to `import x from 'name'`. | ||
*/ | ||
import {exec as execCallback} from 'node:child_process' | ||
import fs from 'node:fs/promises' | ||
import path from 'node:path' | ||
import process from 'node:process' | ||
import {pathToFileURL} from 'node:url' | ||
import {promisify} from 'node:util' | ||
import babel from '@babel/core' | ||
import {resolve} from 'import-meta-resolve' | ||
import {fromMarkdown} from 'mdast-util-from-markdown' | ||
import {headingRange} from 'mdast-util-heading-range' | ||
import {generate} from './generate/index.js' | ||
import {nanoid} from 'nanoid' | ||
import {removePosition} from 'unist-util-remove-position' | ||
import {VFile} from 'vfile' | ||
import {findUp} from 'vfile-find-up' | ||
import {VFileMessage} from 'vfile-message' | ||
const exec = promisify(execCallback) | ||
const relativePrefix = './' | ||
const defaultHeading = 'usage' | ||
/** @type {Readonly<Options>} */ | ||
const emptyOptions = {} | ||
/** | ||
* Plugin to add a usage example to a readme. | ||
* Add a usage example to a readme. | ||
* | ||
* @type {import('unified').Plugin<[Options?]|void[], Root>} | ||
* Looks for the first heading matching `options.heading` (case insensitive), | ||
* removes everything between it and an equal or higher next heading, and replaces | ||
* that with a example. | ||
* | ||
* The example runs in Node.js (so no side effects!). | ||
* Line comments (`//`) are turned into markdown. | ||
* Calls to `console.log()` are exposed as code blocks, containing the logged | ||
* values, so `console.log(1 + 1)` becomes `2`. | ||
* Use a string as the first argument to `log` to use as the language for the code. | ||
* | ||
* You can ignore lines with `remark-usage-ignore-next`: | ||
* | ||
* ```js | ||
* // remark-usage-ignore-next | ||
* const two = sum(1, 1) | ||
* | ||
* // remark-usage-ignore-next 3 | ||
* function sum(a, b) { | ||
* return a + b | ||
* } | ||
* ``` | ||
* | ||
* …if no `skip` is given, 1 line is skipped. | ||
* | ||
* @param {Readonly<Options> | null | undefined} [options] | ||
* Configuration (optional). | ||
* @returns | ||
* Transform. | ||
*/ | ||
export default function remarkUsage(options = {}) { | ||
export default function remarkUsage(options) { | ||
const settings = options || emptyOptions | ||
const header = new RegExp( | ||
'^(' + (options.heading || defaultHeading) + ')$', | ||
'^(' + (settings.heading || defaultHeading) + ')$', | ||
'i' | ||
) | ||
return (tree, file, next) => { | ||
/** @type {{tree: Root, file: VFile, options: Options, exampleInstrumentedPath?: string, nodes?: BlockContent[]}} */ | ||
const ctx = {tree, file, options} | ||
/** | ||
* Transform. | ||
* | ||
* @param {Root} tree | ||
* Tree. | ||
* @param {VFile} file | ||
* File. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
return async function (tree, file) { | ||
let exists = false | ||
@@ -63,39 +201,584 @@ | ||
// node, and generate the example. | ||
headingRange(tree, header, (start, nodes, end) => { | ||
headingRange(tree, header, function () { | ||
exists = true | ||
return [start, ...nodes, end] | ||
}) | ||
if (!exists) { | ||
return next() | ||
return | ||
} | ||
generate.run( | ||
ctx, | ||
/** @type {Callback} */ | ||
(error) => { | ||
// If something failed and there’s an example, remove it. | ||
if (ctx.exampleInstrumentedPath) { | ||
try { | ||
fs.unlinkSync(ctx.exampleInstrumentedPath) | ||
// Catch just to be sure. | ||
/* c8 ignore next */ | ||
} catch {} | ||
const id = 'remark-usage-example-' + nanoid().toLowerCase() | ||
const cwd = file.cwd | ||
const from = file.path | ||
? path.dirname(path.resolve(file.cwd, file.path)) | ||
: file.cwd | ||
const pkg = await findPackage(from) | ||
const name = settings.name || pkg?.value.name || undefined | ||
/** @type {string | undefined} */ | ||
let main | ||
try { | ||
const exports = pkg?.value.exports | ||
const primary = | ||
/* c8 ignore next 2 -- seems useless to have an array, but types have it. */ | ||
exports && typeof exports === 'object' && Array.isArray(exports) | ||
? exports[0] | ||
: exports | ||
const item = | ||
primary && typeof primary === 'object' ? primary['.'] : primary | ||
main = resolve( | ||
relativeModule( | ||
settings.main || | ||
(typeof item === 'string' ? item : undefined) || | ||
pkg?.value.main || | ||
'index.js' | ||
), | ||
pathToFileURL(settings.main ? cwd : pkg?.file.dirname || cwd).href + '/' | ||
) | ||
} catch {} | ||
const example = await findExample(cwd, settings.example) | ||
const instrumented = await instrumentExample(cwd, {example, id, main}) | ||
await run(example, instrumented, id) | ||
// Add example. | ||
headingRange(tree, header, function (start, _, end) { | ||
const tokens = tokenize(String(example)) | ||
const nodes = generate(tokens, { | ||
example: String(example), | ||
instrumented, | ||
name | ||
}) | ||
return [start, ...nodes, end] | ||
}) | ||
} | ||
} | ||
/** | ||
* @param {string} from | ||
* From. | ||
* @returns {Promise<PackageInfo | undefined>} | ||
* Nothing. | ||
*/ | ||
async function findPackage(from) { | ||
const file = await findUp('package.json', from) | ||
if (!file) return | ||
const doc = String(await fs.readFile(file.path)) | ||
/** @type {PackageJson} */ | ||
let value | ||
try { | ||
value = JSON.parse(doc) | ||
} catch (error) { | ||
const cause = /** @type {Error} */ (error) | ||
throw new VFileMessage('Cannot parse `package.json` as JSON', { | ||
cause, | ||
ruleId: 'package-json-invalid', | ||
source: 'remark-usage' | ||
}) | ||
} | ||
return {value, file} | ||
} | ||
/** | ||
* @param {string} cwd | ||
* @param {string | null | undefined} givenExample | ||
* @returns {Promise<VFile>} | ||
*/ | ||
async function findExample(cwd, givenExample) { | ||
const example = givenExample | ||
? findExplicitExample(cwd, givenExample) | ||
: findImplicitExample(cwd) | ||
if (!example) { | ||
throw new VFileMessage( | ||
'Cannot find example file to use, either pass `options.example` or use a name', | ||
{ | ||
ruleId: 'example-missing', | ||
source: 'remark-usage' | ||
} | ||
) | ||
} | ||
const url = new URL(example) | ||
const value = String(await fs.readFile(url)) | ||
return new VFile({ | ||
path: url, | ||
// Make sure there is a final line feed. | ||
value: value.charAt(value.length - 1) === '\n' ? value : value + '\n' | ||
}) | ||
} | ||
/** | ||
* @param {string} cwd | ||
* Base. | ||
* @param {string} example | ||
* Name. | ||
* @returns {string} | ||
* URL. | ||
*/ | ||
function findExplicitExample(cwd, example) { | ||
const from = pathToFileURL(cwd).href + '/' | ||
return resolve(relativeModule(example), from) | ||
} | ||
/** | ||
* @param {string} cwd | ||
* Base. | ||
* @returns {string | undefined} | ||
* URL. | ||
*/ | ||
function findImplicitExample(cwd) { | ||
const from = pathToFileURL(cwd).href + '/' | ||
const examples = [ | ||
'./example.js', | ||
'./example/index.js', | ||
'./examples.js', | ||
'./examples/index.js', | ||
'./doc/example.js', | ||
'./doc/example/index.js', | ||
'./docs/example.js', | ||
'./docs/example/index.js' | ||
] | ||
let index = -1 | ||
while (++index < examples.length) { | ||
const example = examples[index] | ||
try { | ||
return resolve(example, from) | ||
} catch {} | ||
} | ||
} | ||
/** | ||
* @param {string} cwd | ||
* @param {InstrumentOptions} options | ||
* @returns {Promise<InstrumentResult>} | ||
* Nothing. | ||
*/ | ||
async function instrumentExample(cwd, options) { | ||
/** @type {Array<StringLiteral>} */ | ||
const nodes = [] | ||
/** @type {Array<Log>} */ | ||
const logs = [] | ||
/** @type {Array<Reference>} */ | ||
const references = [] | ||
/** @type {FileResult | null} */ | ||
let result | ||
try { | ||
result = await babel.transformAsync(String(options.example), { | ||
caller: {name: 'remark-usage'}, | ||
cwd, | ||
filename: options.example.path, | ||
plugins: [addIdToConsoleLog], | ||
sourceType: 'unambiguous' | ||
}) | ||
} catch (error) { | ||
const cause = /** @type {Error} */ (error) | ||
throw new VFileMessage('Cannot parse example as JS with Babel', { | ||
cause, | ||
ruleId: 'example-invalid-babel', | ||
source: 'remark-usage' | ||
}) | ||
} | ||
let index = -1 | ||
while (++index < nodes.length) { | ||
const node = nodes[index] | ||
const resolved = resolve( | ||
node.value, | ||
pathToFileURL(options.example.path).href | ||
) | ||
if (resolved === options.main) { | ||
/* c8 ignore next -- babel always adds raw, but just to be sure. */ | ||
const raw = String((node && node.extra && node.extra.raw) || "'") | ||
references.push({ | ||
end: node.end, | ||
quote: raw.charAt(0), | ||
start: node.start | ||
}) | ||
} | ||
} | ||
return { | ||
logs, | ||
references, | ||
/* c8 ignore next -- babel always returns a result (though types say it might not). */ | ||
result: result?.code || '' | ||
} | ||
function addIdToConsoleLog() { | ||
const t = babel.types | ||
let index = -1 | ||
/** @type {PluginPass} */ | ||
return { | ||
visitor: { | ||
/** | ||
* @param {NodePathCallExpression} path | ||
* Path. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
CallExpression(path) { | ||
const callee = path.get('callee') | ||
const head = path.get('arguments.0') | ||
if ( | ||
callee.isIdentifier({name: 'require'}) && | ||
!Array.isArray(head) && | ||
head.isStringLiteral() | ||
) { | ||
nodes.push(head.node) | ||
} | ||
if (callee.matchesPattern('console.log')) { | ||
instrumentConsoleLog(path) | ||
} | ||
}, | ||
/** | ||
* @param {NodePathImportDeclaration} path | ||
* Path. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
ImportDeclaration(path) { | ||
nodes.push(path.node.source) | ||
} | ||
} | ||
} | ||
if (!error) { | ||
// Add example. | ||
headingRange(tree, header, (start, _, end) => [ | ||
start, | ||
// `nodes` are always defined. | ||
/* c8 ignore next */ | ||
...(ctx.nodes || []), | ||
end | ||
]) | ||
/** | ||
* @param {NodePathCallExpression} path | ||
* Path. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function instrumentConsoleLog(path) { | ||
const node = path.node | ||
const args = [...node.arguments] | ||
const head = args[0] | ||
/** @type {string | undefined} */ | ||
let language | ||
index++ | ||
if (head && head.type === 'StringLiteral' && args.length > 1) { | ||
language = head.value | ||
args.shift() | ||
} | ||
logs[index] = { | ||
end: node.end, | ||
language, | ||
start: node.start, | ||
values: [] | ||
} | ||
node.arguments = [ | ||
t.stringLiteral('<' + options.id + '-' + index + '>'), | ||
...args, | ||
t.stringLiteral('</' + options.id + '>') | ||
] | ||
} | ||
} | ||
} | ||
/** | ||
* @param {VFile} example | ||
* @param {InstrumentResult} instrumented | ||
* @param {string} id | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function run(example, instrumented, id) { | ||
/* c8 ignore next -- there’s always a dirname. */ | ||
const filePath = path.join(example.dirname || '', id + example.extname) | ||
let stdout = '' | ||
await fs.writeFile(filePath, instrumented.result) | ||
try { | ||
const result = await exec([process.execPath, filePath].join(' ')) | ||
stdout = result.stdout | ||
} catch (error) { | ||
const cause = /** @type {Error} */ (error) | ||
throw new VFileMessage('Cannot run example with Node', { | ||
cause, | ||
ruleId: 'example-invalid-node', | ||
source: 'remark-usage' | ||
}) | ||
} finally { | ||
await fs.unlink(filePath) | ||
} | ||
const open = new RegExp('<' + id + '-(\\d+)>', 'g') | ||
const close = new RegExp('</' + id + '>', 'g') | ||
/** @type {RegExpExecArray | null} */ | ||
let startMatch | ||
while ((startMatch = open.exec(stdout))) { | ||
close.lastIndex = startMatch.index | ||
const endMatch = close.exec(stdout) | ||
// Else should never occur, console is sync, but just to be sure. | ||
if (endMatch) { | ||
const start = startMatch.index + startMatch[0].length | ||
const end = endMatch.index | ||
const logIndex = Number.parseInt(startMatch[1], 10) | ||
const log = instrumented.logs[logIndex] | ||
let value = stdout.slice(start, end) | ||
// Else should never occur, console adds spaces at start and end, just | ||
// to be sure we’re checking it though. | ||
if (value.charAt(0) === ' ' && value.charAt(value.length - 1) === ' ') { | ||
value = value.slice(1, -1) | ||
} | ||
log.values.push(value) | ||
open.lastIndex = end | ||
} | ||
} | ||
} | ||
/** | ||
* @param {string} value | ||
* @returns {Array<Token>} | ||
* Tokens. | ||
*/ | ||
function tokenize(value) { | ||
const lineBreak = /\r?\n/g | ||
const lineComment = /^\s*\/\/\s*/ | ||
const ignoreComment = /^remark-usage-ignore-next(?:\s+(\d+))?/ | ||
/** @type {Array<Token>} */ | ||
const tokens = [] | ||
let start = 0 | ||
let skip = 0 | ||
/** @type {RegExpExecArray | null} */ | ||
let match | ||
/** @type {Token | undefined} */ | ||
let token | ||
while ((match = lineBreak.exec(value))) { | ||
const end = match.index | ||
let line = value.slice(start, end) | ||
// If the line is supposed to be skipped, skip it. | ||
// Skipping can only happen by starting a comment. | ||
if (skip) { | ||
skip-- | ||
} | ||
// Empty: | ||
else if (line.trim().length === 0) { | ||
if (token) { | ||
if (token.type === 'comment') { | ||
token.values.push(match[0]) | ||
} else if (token.type === 'code') { | ||
token.end = end | ||
} | ||
} | ||
} else { | ||
const comment = line.match(lineComment) | ||
return next(error) | ||
if (comment) { | ||
line = line.slice(comment[0].length) | ||
const ignore = line.match(ignoreComment) | ||
if (ignore) { | ||
// Skip next couple of lines. | ||
skip = | ||
(ignore[1] !== undefined && Number.parseInt(ignore[1], 10)) || 1 | ||
token = {type: 'skip', skip} | ||
tokens.push(token) | ||
} else { | ||
if (!token || token.type !== 'comment') { | ||
token = {type: 'comment', values: []} | ||
tokens.push(token) | ||
} | ||
token.values.push(line, match[0]) | ||
} | ||
} else { | ||
if (!token || token.type !== 'code') { | ||
token = {type: 'code', start, end} | ||
tokens.push(token) | ||
} | ||
token.end = end | ||
} | ||
) | ||
} | ||
start = end + match[0].length | ||
} | ||
return tokens | ||
} | ||
/** | ||
* @param {Array<Token>} tokens | ||
* Tokens. | ||
* @param {GenerateOptions} options | ||
* Configuration. | ||
* @returns {Array<RootContent>} | ||
* Result. | ||
*/ | ||
function generate(tokens, options) { | ||
/** @type {Array<RootContent>} */ | ||
const nodes = [] | ||
let index = -1 | ||
while (++index < tokens.length) { | ||
const token = tokens[index] | ||
const result = | ||
token.type === 'comment' | ||
? comment(token) | ||
: token.type === 'code' | ||
? code(token, options) | ||
: undefined | ||
if (result) { | ||
nodes.push(...result) | ||
} | ||
} | ||
return nodes | ||
} | ||
/** | ||
* @param {Comment} token | ||
* Token. | ||
* @returns {Array<RootContent>} | ||
* Result. | ||
*/ | ||
function comment(token) { | ||
const tree = fromMarkdown(token.values.join('')) | ||
removePosition(tree) | ||
return tree.children | ||
} | ||
/** | ||
* @param {Code} token | ||
* Token. | ||
* @param {GenerateOptions} options | ||
* Configuration. | ||
* @returns {Array<RootContent>} | ||
* Result. | ||
*/ | ||
function code(token, options) { | ||
/** @type {Array<CodeWithValues | Log>} */ | ||
const tokens = [] | ||
let start = token.start | ||
/** @type {CodeWithValues | Log | undefined} */ | ||
let tok | ||
while (start < token.end) { | ||
let lineEnd = options.example.indexOf('\n', start) | ||
lineEnd = lineEnd === -1 || lineEnd >= token.end ? token.end : lineEnd | ||
const consoleCall = findInLine(options.instrumented.logs, start, lineEnd) | ||
if (consoleCall && consoleCall.values.length > 0) { | ||
if (tok && tok === consoleCall) { | ||
// Ignore: it’s the same multiline console call. | ||
} else { | ||
tok = consoleCall | ||
tokens.push(tok) | ||
} | ||
} else { | ||
const mainReference = options.name | ||
? findInLine(options.instrumented.references, start, lineEnd) | ||
: undefined | ||
const line = | ||
mainReference && | ||
typeof mainReference.start === 'number' && | ||
typeof mainReference.end === 'number' | ||
? options.example.slice(start, mainReference.start) + | ||
mainReference.quote + | ||
options.name + | ||
mainReference.quote + | ||
options.example.slice(mainReference.end, lineEnd) | ||
: options.example.slice(start, lineEnd) | ||
if (!tok || !('type' in tok) || tok.type !== 'code') { | ||
tok = {type: 'code', values: []} | ||
tokens.push(tok) | ||
} | ||
tok.values.push(line) | ||
} | ||
start = lineEnd + 1 | ||
} | ||
/** @type {Array<RootContent>} */ | ||
const nodes = [] | ||
let index = -1 | ||
while (++index < tokens.length) { | ||
const token = tokens[index] | ||
nodes.push({ | ||
type: 'code', | ||
lang: 'language' in token ? token.language : 'javascript', | ||
value: token.values.join('\n').replace(/^\n+|\n+$/g, '') | ||
}) | ||
} | ||
return nodes | ||
} | ||
/** | ||
* @template {Log | Reference} Value | ||
* Token kind. | ||
* @param {Array<Value>} values | ||
* Values. | ||
* @param {number} start | ||
* Start. | ||
* @param {number} end | ||
* End. | ||
* @returns {Value | undefined} | ||
* Found. | ||
*/ | ||
function findInLine(values, start, end) { | ||
let index = -1 | ||
while (++index < values.length) { | ||
const reference = values[index] | ||
if ( | ||
typeof reference.start === 'number' && | ||
typeof reference.end === 'number' && | ||
// Reference in: | ||
((reference.start >= start && reference.end <= end) || | ||
// Line in reference: | ||
(start >= reference.start && end <= reference.end)) | ||
) { | ||
return reference | ||
} | ||
} | ||
} | ||
/** | ||
* Make a path relative. | ||
* | ||
* @param {string} moduleId | ||
* Specifier. | ||
* @returns {string} | ||
* Relative specifier. | ||
*/ | ||
function relativeModule(moduleId) { | ||
return moduleId.slice(0, 2) === relativePrefix | ||
? moduleId | ||
: relativePrefix + moduleId | ||
} |
116
package.json
{ | ||
"name": "remark-usage", | ||
"version": "10.0.1", | ||
"version": "11.0.0", | ||
"description": "remark plugin to add a usage example to your readme", | ||
"license": "MIT", | ||
"keywords": [ | ||
"unified", | ||
"markdown", | ||
"mdast", | ||
"plain", | ||
"plugin", | ||
"remark", | ||
"remark-plugin", | ||
"plugin", | ||
"mdast", | ||
"markdown", | ||
"plain", | ||
"text" | ||
"text", | ||
"unified" | ||
], | ||
@@ -30,4 +30,3 @@ "repository": "remarkjs/remark-usage", | ||
"type": "module", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"exports": "./index.js", | ||
"files": [ | ||
@@ -40,63 +39,45 @@ "lib/", | ||
"@babel/core": "^7.0.0", | ||
"@types/mdast": "^3.0.0", | ||
"import-meta-resolve": "^1.0.0", | ||
"mdast-util-heading-range": "^3.0.0", | ||
"nanoid": "^3.0.0", | ||
"remark-parse": "^10.0.0", | ||
"to-vfile": "^7.0.0", | ||
"trough": "^2.0.0", | ||
"unified": "^10.0.0", | ||
"unist-util-remove-position": "^4.0.0", | ||
"vfile": "^5.0.2", | ||
"vfile-find-up": "^6.0.0" | ||
"@types/mdast": "^4.0.0", | ||
"import-meta-resolve": "^3.0.0", | ||
"mdast-util-from-markdown": "^2.0.0", | ||
"mdast-util-heading-range": "^4.0.0", | ||
"nanoid": "^4.0.0", | ||
"unist-util-remove-position": "^5.0.0", | ||
"vfile": "^6.0.0", | ||
"vfile-find-up": "^7.0.0", | ||
"vfile-message": "^4.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/babel__core": "^7.0.0", | ||
"@types/tape": "^4.0.0", | ||
"c8": "^7.0.0", | ||
"is-hidden": "^2.0.0", | ||
"prettier": "^2.0.0", | ||
"remark": "^14.0.0", | ||
"remark-cli": "^10.0.0", | ||
"@types/node": "^20.0.0", | ||
"c8": "^8.0.0", | ||
"prettier": "^3.0.0", | ||
"remark": "^15.0.0", | ||
"remark-cli": "^11.0.0", | ||
"remark-preset-wooorm": "^9.0.0", | ||
"rimraf": "^3.0.0", | ||
"tape": "^5.0.0", | ||
"to-vfile": "^8.0.0", | ||
"type-coverage": "^2.0.0", | ||
"type-fest": "^2.0.0", | ||
"typescript": "^4.0.0", | ||
"xo": "^0.45.0" | ||
"type-fest": "^4.0.0", | ||
"typescript": "^5.0.0", | ||
"xo": "^0.56.0" | ||
}, | ||
"scripts": { | ||
"build": "rimraf \"lib/**/*.d.ts\" \"test/index.d.ts\" \"index.d.ts\" && tsc && type-coverage", | ||
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", | ||
"build": "tsc --build --clean && tsc --build && type-coverage", | ||
"format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", | ||
"prepack": "npm run build && npm run format", | ||
"test": "npm run build && npm run format && npm run test-coverage", | ||
"test-api": "node --conditions development test/index.js", | ||
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api", | ||
"test": "npm run build && npm run format && npm run test-coverage" | ||
"test-coverage": "c8 --100 --reporter lcov npm run test-api" | ||
}, | ||
"prettier": { | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"bracketSpacing": false, | ||
"singleQuote": true, | ||
"bracketSpacing": false, | ||
"semi": false, | ||
"trailingComma": "none" | ||
"tabWidth": 2, | ||
"trailingComma": "none", | ||
"useTabs": false | ||
}, | ||
"xo": { | ||
"prettier": true, | ||
"ignores": [ | ||
"example.js", | ||
"test/fixtures/**/*.js" | ||
], | ||
"overrides": [ | ||
{ | ||
"files": "test/**/*.js", | ||
"rules": { | ||
"no-await-in-loop": "off" | ||
} | ||
} | ||
] | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"preset-wooorm", | ||
"remark-preset-wooorm", | ||
[ | ||
@@ -113,5 +94,26 @@ "./index.js", | ||
"detail": true, | ||
"strict": true, | ||
"ignoreCatch": true | ||
"ignoreCatch": true, | ||
"strict": true | ||
}, | ||
"xo": { | ||
"ignores": [ | ||
"example.js", | ||
"test/fixtures/**/*.js" | ||
], | ||
"overrides": [ | ||
{ | ||
"files": [ | ||
"test/**/*.js" | ||
], | ||
"rules": { | ||
"no-await-in-loop": "off" | ||
} | ||
} | ||
], | ||
"prettier": true, | ||
"rules": { | ||
"unicorn/prefer-at": "off", | ||
"unicorn/prefer-string-replace-all": "off" | ||
} | ||
} | ||
} |
134
readme.md
@@ -6,3 +6,2 @@ # remark-usage | ||
[![Downloads][downloads-badge]][downloads] | ||
[![Size][size-badge]][size] | ||
[![Sponsors][sponsors-badge]][collective] | ||
@@ -12,3 +11,3 @@ [![Backers][backers-badge]][collective] | ||
[**remark**][remark] plugin to add a [usage][] example to a readme. | ||
**[remark][]** plugin to add a [usage][section-use] example to a readme. | ||
@@ -23,2 +22,3 @@ ## Contents | ||
* [`unified().use(remarkUsage[, options])`](#unifieduseremarkusage-options) | ||
* [`Options`](#options) | ||
* [Types](#types) | ||
@@ -33,12 +33,5 @@ * [Compatibility](#compatibility) | ||
This package is a [unified][] ([remark][]) plugin to add a Usage section to | ||
This package is a [unified][] ([remark][]) plugin to add a usage section to | ||
markdown. | ||
unified is an AST (abstract syntax tree) based transform project. | ||
**remark** is everything unified that relates to markdown. | ||
The layer under remark is called mdast, which is only concerned with syntax | ||
trees. | ||
Another layer underneath is micromark, which is only concerned with parsing. | ||
This package is a small wrapper to integrate all of these. | ||
## When should I use this? | ||
@@ -51,4 +44,4 @@ | ||
This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c). | ||
In Node.js (12.20+, 14.14+, 16.0+), install with [npm][]: | ||
This package is [ESM only][esm]. | ||
In Node.js (version 16+), install with [npm][]: | ||
@@ -89,15 +82,15 @@ ```sh | ||
…If we use `remark-usage`, we can generate the `Usage` section | ||
…if we use `remark-usage`, we can generate the `Usage` section | ||
```javascript | ||
import {readSync} from 'to-vfile' | ||
import {remark} from 'remark' | ||
import remarkUsage from 'remark-usage' | ||
import {read} from 'to-vfile' | ||
const file = readSync({path: 'readme.md', cwd: 'example'}) | ||
const file = await read({path: 'readme.md', cwd: 'example'}) | ||
const result = await remark().use(remarkUsage).process(file) | ||
await remark().use(remarkUsage).process(file) | ||
``` | ||
Now, printing `result` (the newly generated readme) yields: | ||
…then printing `file` (the newly generated readme) yields: | ||
@@ -131,23 +124,20 @@ ````markdown | ||
This package exports no identifiers. | ||
The default export is `remarkUsage`. | ||
The default export is [`remarkUsage`][api-remark-usage]. | ||
### `unified().use(remarkUsage[, options])` | ||
Add `example.js` to the `Usage` section in a readme. | ||
Add a usage example to a readme. | ||
Replaces the current content between the heading containing the text “usage” | ||
(configurable) and the next heading of the same (or higher) rank with the | ||
example. | ||
Looks for the first heading matching `options.heading` (case insensitive), | ||
removes everything between it and an equal or higher next heading, and replaces | ||
that with an example. | ||
The example is run in Node.js. | ||
Make sure no side effects occur when running `example.js`. | ||
Line comments are parsed as markdown. | ||
The example runs in Node.js (so no side effects!). | ||
Line comments (`//`) are turned into markdown. | ||
Calls to `console.log()` are exposed as code blocks, containing the logged | ||
values (optionally with a language flag). | ||
values, so `console.log(1 + 1)` becomes `2`. | ||
Use a string as the first argument to `log` to use as the language for the code. | ||
It may help to compare [`example.js`][example-js] with the above [use][usage] | ||
section. | ||
You can ignore lines with `remark-usage-ignore-next`: | ||
You can ignore lines like so: | ||
```js | ||
@@ -165,46 +155,52 @@ // remark-usage-ignore-next | ||
##### `options` | ||
###### Parameters | ||
###### `options.heading` | ||
* `options` ([`Options`][api-options], optional) | ||
— configuration | ||
Heading to look for (`string?`, default: `'usage'`). | ||
Wrapped in `new RegExp('^(' + value + ')$', 'i');`. | ||
###### Returns | ||
###### `options.example` | ||
Transform ([`Transformer`][unified-transformer]). | ||
Path to the example (`string?`). | ||
If given, resolved from [`file.cwd`][file-cwd]. | ||
If not given, the following values are attempted and resolved from `file.cwd`: | ||
`'./example.js'`, `'./example/index.js'`, `'./examples.js'`, | ||
`'./examples/index.js'`, `'./doc/example.js'`, `'./doc/example/index.js'`, | ||
`'./docs/example.js'`, `'./docs/example/index.js'`. | ||
The first that exists, is used. | ||
### `Options` | ||
###### `options.name` | ||
Configuration (TypeScript type). | ||
Name of the module (`string?`, default: `pkg.name`, optional). | ||
Used to rewrite `require('.')` to `require('name')`. | ||
###### Fields | ||
###### `options.main` | ||
* `example` (`string`, optional) | ||
— path to example file (optional); | ||
resolved from `file.cwd`; | ||
defaults to the first example that exists: `'example.js'`, | ||
`'example/index.js'`, `'examples.js'`, `'examples/index.js'`, | ||
`'doc/example.js'`, `'doc/example/index.js'`, `'docs/example.js'`, | ||
`'docs/example/index.js'` | ||
* `heading` (`string`, default: `'usage'`) | ||
— heading to look for; | ||
wrapped in `new RegExp('^(' + value + ')$', 'i');` | ||
* `main` (`string`, default: `pkg.exports`, `pkg.main`, `'index.js'`) | ||
— path to the file; | ||
resolved from `file.cwd`; | ||
used to rewrite `import x from './main.js'` to `import x from 'name'` | ||
* `name` (`string`, default: `pkg.name`) | ||
— name of the module; | ||
used to rewrite `import x from './main.js'` to `import x from 'name'` | ||
Path to the main file (`string?`, default: `pkg.main` or `'.'`, optional). | ||
If given, resolved from [`file.cwd`][file-cwd]. | ||
If inferred from `package.json`, resolved relating to that package root. | ||
Used to rewrite `require('.')` to `require('name')`. | ||
## Types | ||
This package is fully typed with [TypeScript][]. | ||
It exports an `Options` type, which specifies the interface of the accepted | ||
options. | ||
It exports the additional type [`Options`][api-options]. | ||
## Compatibility | ||
Projects maintained by the unified collective are compatible with all maintained | ||
Projects maintained by the unified collective are compatible with maintained | ||
versions of Node.js. | ||
As of now, that is Node.js 12.20+, 14.14+, and 16.0+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
This plugin works with remark 12+ and `remark-cli` 8+. | ||
When we cut a new major release, we drop support for unmaintained versions of | ||
Node. | ||
This means we try to keep the current release line, `remark-usage@^11`, | ||
compatible with Node.js 16. | ||
This plugin works with remark version 12+ and `remark-cli` version 8+. | ||
## Security | ||
@@ -253,6 +249,2 @@ | ||
[size-badge]: https://img.shields.io/bundlephobia/minzip/remark-usage.svg | ||
[size]: https://bundlephobia.com/result?p=remark-usage | ||
[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg | ||
@@ -270,9 +262,11 @@ | ||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c | ||
[health]: https://github.com/remarkjs/.github | ||
[contributing]: https://github.com/remarkjs/.github/blob/HEAD/contributing.md | ||
[contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md | ||
[support]: https://github.com/remarkjs/.github/blob/HEAD/support.md | ||
[support]: https://github.com/remarkjs/.github/blob/main/support.md | ||
[coc]: https://github.com/remarkjs/.github/blob/HEAD/code-of-conduct.md | ||
[coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md | ||
@@ -283,4 +277,2 @@ [license]: license | ||
[unified]: https://github.com/unifiedjs/unified | ||
[remark]: https://github.com/remarkjs/remark | ||
@@ -290,6 +282,10 @@ | ||
[file-cwd]: https://github.com/vfile/vfile#vfilecwd | ||
[unified]: https://github.com/unifiedjs/unified | ||
[usage]: #use | ||
[unified-transformer]: https://github.com/unifiedjs/unified#transformer | ||
[example-js]: example.js | ||
[section-use]: #use | ||
[api-options]: #options | ||
[api-remark-usage]: #unifieduseremarkusage-options |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
10
12
35446
7
926
280
1
+ Addedvfile-message@^4.0.0
+ Added@types/mdast@4.0.4(transitive)
+ Added@types/unist@3.0.3(transitive)
+ Addeddevlop@1.1.0(transitive)
+ Addedimport-meta-resolve@3.1.1(transitive)
+ Addedmdast-util-from-markdown@2.0.1(transitive)
+ Addedmdast-util-heading-range@4.0.0(transitive)
+ Addedmdast-util-to-string@4.0.0(transitive)
+ Addedmicromark@4.0.0(transitive)
+ Addedmicromark-core-commonmark@2.0.1(transitive)
+ Addedmicromark-factory-destination@2.0.0(transitive)
+ Addedmicromark-factory-label@2.0.0(transitive)
+ Addedmicromark-factory-space@2.0.0(transitive)
+ Addedmicromark-factory-title@2.0.0(transitive)
+ Addedmicromark-factory-whitespace@2.0.0(transitive)
+ Addedmicromark-util-character@2.1.0(transitive)
+ Addedmicromark-util-chunked@2.0.0(transitive)
+ Addedmicromark-util-classify-character@2.0.0(transitive)
+ Addedmicromark-util-combine-extensions@2.0.0(transitive)
+ Addedmicromark-util-decode-numeric-character-reference@2.0.1(transitive)
+ Addedmicromark-util-decode-string@2.0.0(transitive)
+ Addedmicromark-util-encode@2.0.0(transitive)
+ Addedmicromark-util-html-tag-name@2.0.0(transitive)
+ Addedmicromark-util-normalize-identifier@2.0.0(transitive)
+ Addedmicromark-util-resolve-all@2.0.0(transitive)
+ Addedmicromark-util-sanitize-uri@2.0.0(transitive)
+ Addedmicromark-util-subtokenize@2.0.1(transitive)
+ Addedmicromark-util-symbol@2.0.0(transitive)
+ Addedmicromark-util-types@2.0.0(transitive)
+ Addednanoid@4.0.2(transitive)
+ Addedunist-util-is@6.0.0(transitive)
+ Addedunist-util-remove-position@5.0.0(transitive)
+ Addedunist-util-stringify-position@4.0.0(transitive)
+ Addedunist-util-visit@5.0.0(transitive)
+ Addedunist-util-visit-parents@6.0.1(transitive)
+ Addedvfile@6.0.3(transitive)
+ Addedvfile-find-up@7.1.0(transitive)
+ Addedvfile-message@4.0.2(transitive)
- Removedremark-parse@^10.0.0
- Removedto-vfile@^7.0.0
- Removedtrough@^2.0.0
- Removedunified@^10.0.0
- Removed@types/mdast@3.0.15(transitive)
- Removed@types/unist@2.0.11(transitive)
- Removedbail@2.0.2(transitive)
- Removedbuiltins@4.1.0(transitive)
- Removeddiff@5.2.0(transitive)
- Removedextend@3.0.2(transitive)
- Removedimport-meta-resolve@1.1.1(transitive)
- Removedis-buffer@2.0.5(transitive)
- Removedis-plain-obj@4.1.0(transitive)
- Removedkleur@4.1.5(transitive)
- Removedmdast-util-from-markdown@1.3.1(transitive)
- Removedmdast-util-heading-range@3.1.1(transitive)
- Removedmdast-util-to-string@3.2.0(transitive)
- Removedmicromark@3.2.0(transitive)
- Removedmicromark-core-commonmark@1.1.0(transitive)
- Removedmicromark-factory-destination@1.1.0(transitive)
- Removedmicromark-factory-label@1.1.0(transitive)
- Removedmicromark-factory-space@1.1.0(transitive)
- Removedmicromark-factory-title@1.1.0(transitive)
- Removedmicromark-factory-whitespace@1.1.0(transitive)
- Removedmicromark-util-character@1.2.0(transitive)
- Removedmicromark-util-chunked@1.1.0(transitive)
- Removedmicromark-util-classify-character@1.1.0(transitive)
- Removedmicromark-util-combine-extensions@1.1.0(transitive)
- Removedmicromark-util-decode-numeric-character-reference@1.1.0(transitive)
- Removedmicromark-util-decode-string@1.1.0(transitive)
- Removedmicromark-util-encode@1.1.0(transitive)
- Removedmicromark-util-html-tag-name@1.2.0(transitive)
- Removedmicromark-util-normalize-identifier@1.1.0(transitive)
- Removedmicromark-util-resolve-all@1.1.0(transitive)
- Removedmicromark-util-sanitize-uri@1.2.0(transitive)
- Removedmicromark-util-subtokenize@1.1.0(transitive)
- Removedmicromark-util-symbol@1.1.0(transitive)
- Removedmicromark-util-types@1.1.0(transitive)
- Removedmri@1.2.0(transitive)
- Removednanoid@3.3.7(transitive)
- Removedremark-parse@10.0.2(transitive)
- Removedsade@1.8.1(transitive)
- Removedsemver@7.6.3(transitive)
- Removedto-vfile@7.2.4(transitive)
- Removedtrough@2.2.0(transitive)
- Removedunified@10.1.2(transitive)
- Removedunist-util-is@5.2.1(transitive)
- Removedunist-util-remove-position@4.0.2(transitive)
- Removedunist-util-stringify-position@3.0.3(transitive)
- Removedunist-util-visit@4.1.2(transitive)
- Removedunist-util-visit-parents@5.1.3(transitive)
- Removeduvu@0.5.6(transitive)
- Removedvfile@5.3.7(transitive)
- Removedvfile-find-up@6.1.0(transitive)
- Removedvfile-message@3.1.4(transitive)
Updated@types/mdast@^4.0.0
Updatedimport-meta-resolve@^3.0.0
Updatednanoid@^4.0.0
Updatedvfile@^6.0.0
Updatedvfile-find-up@^7.0.0