unified-engine
Advanced tools
Comparing version
@@ -1,140 +0,313 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('unified').Plugin<Array<unknown>>} Plugin | ||
* @typedef {import('unified').PluginTuple<Array<unknown>>} PluginTuple | ||
* @typedef {import('unified').Preset} Preset | ||
* @typedef {import('unified').Settings} Settings | ||
*/ | ||
var path = require('path') | ||
var Module = require('module') | ||
var yaml = require('js-yaml') | ||
var json = require('parse-json') | ||
var debug = require('debug')('unified-engine:configuration') | ||
var resolve = require('load-plugin').resolve | ||
var fault = require('fault') | ||
var xtend = require('xtend') | ||
var object = require('is-object') | ||
var string = require('x-is-string') | ||
var FindUp = require('./find-up') | ||
/** | ||
* @callback Callback | ||
* Callback called when loading a config. | ||
* @param {Error | undefined} error | ||
* Error if something happened. | ||
* @param {Result | undefined} [result] | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
* | ||
* @callback ConfigTransform | ||
* Transform arbitrary configs to our format. | ||
* @param {any} config | ||
* Arbitrary config. | ||
* @param {string} filePath | ||
* File path of config file. | ||
* @returns {PresetSupportingSpecifiers} | ||
* Our config format. | ||
* | ||
* @typedef ImportResult | ||
* Result from an `import` call. | ||
* @property {unknown} [default] | ||
* Default field. | ||
* | ||
* Note: we can’t use `@-callback` because TS doesn’t deal with `this` correctly. | ||
* @typedef {(this: Configuration, buf: Buffer, filePath: string) => Promise<PresetSupportingSpecifiers | undefined>} Loader | ||
* Loader for different config files. | ||
* | ||
* | ||
* @typedef MergeConfiguration | ||
* How to merge. | ||
* @property {string | undefined} prefix | ||
* Plugin prefix. | ||
* @property {string} root | ||
* File path to merge from. | ||
* | ||
* Used to resolve plugins. | ||
* | ||
* @typedef {Array<PluggableSupportingSpecifiers>} PluggableListSupportingSpecifiers | ||
* List of plugins and configuration, with support for specifiers. | ||
* | ||
* @typedef {Record<string, unknown>} PluggableMap | ||
* Map where each key is a plugin specifier and each value is its primary parameter. | ||
* | ||
* @typedef {PluginSupportingSpecifiers | PluginTupleSupportingSpecifiers | Preset} PluggableSupportingSpecifiers | ||
* Usable values, with support for specifiers. | ||
* | ||
* @typedef {Plugin | string} PluginSupportingSpecifiers | ||
* A plugin, or a specifier to one. | ||
* | ||
* @typedef {[plugin: string, ...parameters: Array<unknown>] | PluginTuple} PluginTupleSupportingSpecifiers | ||
* A plugin with configuration, with support for specifiers. | ||
* | ||
* @typedef PresetSupportingSpecifiers | ||
* Sharable configuration, with support for specifiers. | ||
* | ||
* Specifiers should *not* be used in actual presets (because they can’t be | ||
* used by regular unified), but they can be used in config files locally, | ||
* as those are only for the engine. | ||
* | ||
* They can contain plugins and settings. | ||
* @property {PluggableListSupportingSpecifiers | PluggableMap | undefined} [plugins] | ||
* List of plugins and presets (optional). | ||
* @property {Settings | undefined} [settings] | ||
* Shared settings for parsers and compilers (optional). | ||
* | ||
* @typedef Result | ||
* Resolved configuration. | ||
* @property {string | undefined} filePath | ||
* File path of found configuration. | ||
* @property {Settings} settings | ||
* Resolved settings. | ||
* @property {Array<PluginTuple>} plugins | ||
* Resolved plugins. | ||
* | ||
*/ | ||
module.exports = Config | ||
import assert from 'node:assert/strict' | ||
import path from 'node:path' | ||
import {pathToFileURL} from 'node:url' | ||
import structuredClone from '@ungap/structured-clone' | ||
import createDebug from 'debug' | ||
import isPlainObj from 'is-plain-obj' | ||
import {resolvePlugin} from 'load-plugin' | ||
import parseJson from 'parse-json' | ||
import yaml from 'yaml' | ||
import {FindUp} from './find-up.js' | ||
var own = {}.hasOwnProperty | ||
var extname = path.extname | ||
var basename = path.basename | ||
var dirname = path.dirname | ||
var relative = path.relative | ||
const debug = createDebug('unified-engine:configuration') | ||
var loaders = { | ||
'.json': loadJSON, | ||
'.js': loadScript, | ||
'.yaml': loadYAML, | ||
'.yml': loadYAML | ||
const own = {}.hasOwnProperty | ||
/** @type {Record<string, Loader>} */ | ||
const loaders = { | ||
'.json': loadJson, | ||
'.cjs': loadScriptOrModule, | ||
'.mjs': loadScriptOrModule, | ||
'.js': loadScriptOrModule, | ||
'.yaml': loadYaml, | ||
'.yml': loadYaml | ||
} | ||
var defaultLoader = loadJSON | ||
const defaultLoader = loadJson | ||
Config.prototype.load = load | ||
/** | ||
* @typedef Options | ||
* Configuration. | ||
* @property {ConfigTransform | undefined} [configTransform] | ||
* Transform config files from a different schema (optional). | ||
* @property {string} cwd | ||
* Base (required). | ||
* @property {PresetSupportingSpecifiers | undefined} [defaultConfig] | ||
* Default configuration to use if no config file is given or found | ||
* (optional). | ||
* @property {boolean | undefined} [detectConfig] | ||
* Whether to search for configuration files. | ||
* @property {string | undefined} [packageField] | ||
* Field where configuration can be found in `package.json` files | ||
* (optional). | ||
* @property {string | undefined} [pluginPrefix] | ||
* Prefix to use when searching for plugins (optional). | ||
* @property {PluggableListSupportingSpecifiers | PluggableMap | undefined} [plugins] | ||
* Plugins to use (optional). | ||
* @property {string | undefined} [rcName] | ||
* Name of configuration files to load (optional). | ||
* @property {string | undefined} [rcPath] | ||
* Filepath to a configuration file to load (optional). | ||
* @property {Settings | undefined} [settings] | ||
* Configuration for the parser and compiler of the processor (optional). | ||
*/ | ||
function Config(options) { | ||
var rcName = options.rcName | ||
var packageField = options.packageField | ||
var names = [] | ||
export class Configuration { | ||
/** | ||
* Internal class to load configuration files. | ||
* | ||
* Exposed to build more complex integrations. | ||
* | ||
* @param {Options} options | ||
* Configuration (required). | ||
* @returns | ||
* Self. | ||
*/ | ||
constructor(options) { | ||
/** @type {Array<string>} */ | ||
const names = [] | ||
this.cwd = options.cwd | ||
this.packageField = options.packageField | ||
this.pluginPrefix = options.pluginPrefix | ||
this.configTransform = options.configTransform | ||
this.defaultConfig = options.defaultConfig | ||
/** @type {string} */ | ||
this.cwd = options.cwd | ||
/** @type {string | undefined} */ | ||
this.packageField = options.packageField | ||
/** @type {string | undefined} */ | ||
this.pluginPrefix = options.pluginPrefix | ||
/** @type {ConfigTransform | undefined} */ | ||
this.configTransform = options.configTransform | ||
/** @type {PresetSupportingSpecifiers | undefined} */ | ||
this.defaultConfig = options.defaultConfig | ||
if (rcName) { | ||
names.push(rcName, rcName + '.js', rcName + '.yml', rcName + '.yaml') | ||
debug('Looking for `%s` configuration files', names) | ||
} | ||
if (options.rcName) { | ||
names.push( | ||
options.rcName, | ||
...Object.keys(loaders).map(function (d) { | ||
return options.rcName + d | ||
}) | ||
) | ||
debug('Looking for `%s` configuration files', names) | ||
} | ||
if (packageField) { | ||
names.push('package.json') | ||
debug('Looking for `%s` fields in `package.json` files', packageField) | ||
} | ||
if (options.packageField) { | ||
names.push('package.json') | ||
debug( | ||
'Looking for `%s` fields in `package.json` files', | ||
options.packageField | ||
) | ||
} | ||
this.given = {settings: options.settings, plugins: options.plugins} | ||
this.create = create.bind(this) | ||
/** @type {PresetSupportingSpecifiers} */ | ||
this.given = {plugins: options.plugins, settings: options.settings} | ||
this.create = this.create.bind(this) | ||
this.findUp = new FindUp({ | ||
filePath: options.rcPath, | ||
cwd: options.cwd, | ||
detect: options.detectConfig, | ||
names: names, | ||
create: this.create | ||
}) | ||
} | ||
/** @type {FindUp<Result>} */ | ||
this.findUp = new FindUp({ | ||
create: this.create, | ||
cwd: options.cwd, | ||
detect: options.detectConfig, | ||
filePath: options.rcPath, | ||
names | ||
}) | ||
} | ||
function load(filePath, callback) { | ||
var searchPath = filePath || path.resolve(this.cwd, 'stdin.js') | ||
var self = this | ||
/** | ||
* Get the config for a file. | ||
* | ||
* @param {string} filePath | ||
* File path to load. | ||
* @param {Callback} callback | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
load(filePath, callback) { | ||
const self = this | ||
self.findUp.load(searchPath, done) | ||
this.findUp.load( | ||
filePath || path.resolve(this.cwd, 'stdin.js'), | ||
function (error, file) { | ||
if (error || file) { | ||
return callback(error, file) | ||
} | ||
function done(err, res) { | ||
if (err || res) { | ||
return callback(err, res) | ||
} | ||
callback(null, self.create()) | ||
self.create(undefined, undefined).then(function (result) { | ||
callback(undefined, result) | ||
}, callback) | ||
} | ||
) | ||
} | ||
} | ||
function create(buf, filePath) { | ||
var self = this | ||
var transform = self.configTransform | ||
var defaults = self.defaultConfig | ||
var fn = (filePath && loaders[extname(filePath)]) || defaultLoader | ||
var options = {prefix: self.pluginPrefix, cwd: self.cwd} | ||
var result = {settings: {}, plugins: []} | ||
var contents = buf ? fn.apply(self, arguments) : undefined | ||
/** | ||
* This is an internal method, consider it private. | ||
* | ||
* @param {Buffer | undefined} buf | ||
* File value. | ||
* @param {string | undefined} filePath | ||
* File path. | ||
* @returns {Promise<Result | undefined>} | ||
* Result. | ||
*/ | ||
async create(buf, filePath) { | ||
const options = {cwd: this.cwd, prefix: this.pluginPrefix} | ||
/** @type {Result} */ | ||
const result = {filePath: undefined, plugins: [], settings: {}} | ||
const extname = filePath ? path.extname(filePath) : undefined | ||
const loader = | ||
extname && extname in loaders ? loaders[extname] : defaultLoader | ||
/** @type {PresetSupportingSpecifiers | undefined} */ | ||
let value | ||
if (transform && contents !== undefined) { | ||
contents = transform(contents, filePath) | ||
} | ||
if (filePath && buf) { | ||
value = await loader.call(this, buf, filePath) | ||
/* Exit if we did find a `package.json`, but it doesn’t have configuration. */ | ||
if (buf && contents === undefined && basename(filePath) === 'package.json') { | ||
return | ||
} | ||
if (this.configTransform && value !== undefined) { | ||
value = this.configTransform(value, filePath) | ||
} | ||
} | ||
if (contents === undefined) { | ||
if (defaults) { | ||
merge(result, defaults, null, xtend(options, {root: self.cwd})) | ||
// Exit if we did find a `package.json`, but it does not have configuration. | ||
if ( | ||
filePath && | ||
path.basename(filePath) === 'package.json' && | ||
value === undefined | ||
) { | ||
filePath = undefined | ||
} | ||
} else { | ||
merge(result, contents, null, xtend(options, {root: dirname(filePath)})) | ||
} | ||
merge(result, self.given, null, xtend(options, {root: self.cwd})) | ||
if (value === undefined) { | ||
if (this.defaultConfig) { | ||
await merge(result, this.defaultConfig, {...options, root: this.cwd}) | ||
} | ||
} else { | ||
assert(typeof filePath === 'string', 'Expected `filePath` to be set') | ||
await merge(result, value, {...options, root: path.dirname(filePath)}) | ||
} | ||
return result | ||
} | ||
await merge(result, this.given, {...options, root: this.cwd}) | ||
/* Basically `Module.prototype.load`, but for a buffer instead | ||
* of a filepath. */ | ||
function loadScript(buf, filePath) { | ||
var submodule = Module._cache[filePath] | ||
result.filePath = filePath | ||
if (!submodule) { | ||
submodule = new Module(filePath, module) | ||
submodule.filename = filePath | ||
submodule.paths = Module._nodeModulePaths(dirname(filePath)) | ||
submodule._compile(String(buf), filePath) | ||
submodule.loaded = true | ||
Module._cache[filePath] = submodule | ||
return result | ||
} | ||
} | ||
return submodule.exports | ||
/** | ||
* @this {Configuration} | ||
* Class. | ||
* @type {Loader} | ||
* Loader. | ||
*/ | ||
async function loadScriptOrModule(_, filePath) { | ||
// Assume it’s a config. | ||
const result = /** @type {Result} */ ( | ||
await loadFromAbsolutePath(filePath, this.cwd) | ||
) | ||
return result | ||
} | ||
function loadYAML(buf, filePath) { | ||
return yaml.safeLoad(buf, {filename: basename(filePath)}) | ||
/** @type {Loader} */ | ||
async function loadYaml(buf) { | ||
return yaml.parse(String(buf)) | ||
} | ||
function loadJSON(buf, filePath) { | ||
var result = json(buf, filePath) | ||
/** | ||
* @this {Configuration} | ||
* Class. | ||
* @type {Loader} | ||
* Loader. | ||
*/ | ||
async function loadJson(buf, filePath) { | ||
/** @type {Record<string, unknown>} */ | ||
const data = parseJson(String(buf), filePath) | ||
if (basename(filePath) === 'package.json') { | ||
result = result[this.packageField] | ||
} | ||
// Assume it’s a config. | ||
const result = /** @type {Result} */ ( | ||
this.packageField && path.basename(filePath) === 'package.json' | ||
? data[this.packageField] | ||
: data | ||
) | ||
@@ -144,9 +317,15 @@ return result | ||
function merge(target, raw, val, options) { | ||
var root = options.root | ||
var cwd = options.cwd | ||
var prefix = options.prefix | ||
if (object(raw)) { | ||
addPreset(raw) | ||
/** | ||
* @param {Result} target | ||
* Result to merge into. | ||
* @param {PresetSupportingSpecifiers} raw | ||
* Raw found config. | ||
* @param {MergeConfiguration} options | ||
* Configuration. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function merge(target, raw, options) { | ||
if (raw !== null && typeof raw === 'object') { | ||
await addPreset(raw) | ||
} else { | ||
@@ -156,15 +335,15 @@ throw new Error('Expected preset, not `' + raw + '`') | ||
return target | ||
/** | ||
* @param {PresetSupportingSpecifiers} result | ||
* Configuration file. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function addPreset(result) { | ||
const plugins = result.plugins | ||
function addPreset(result) { | ||
var plugins = result.plugins | ||
if (plugins === null || plugins === undefined) { | ||
/* Empty. */ | ||
} else if (object(plugins)) { | ||
if ('length' in plugins) { | ||
addEach(plugins) | ||
} else { | ||
addIn(plugins) | ||
} | ||
// Empty. | ||
} else if (plugins !== null && typeof plugins === 'object') { | ||
await (Array.isArray(plugins) ? addEach(plugins) : addIn(plugins)) | ||
} else { | ||
@@ -176,17 +355,23 @@ throw new Error( | ||
target.settings = xtend(target.settings, result.settings) | ||
target.settings = structuredClone({...target.settings, ...result.settings}) | ||
} | ||
function addEach(result) { | ||
var length = result.length | ||
var index = -1 | ||
var value | ||
/** | ||
* @param {PluggableListSupportingSpecifiers} result | ||
* List of plugins. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function addEach(result) { | ||
let index = -1 | ||
while (++index < length) { | ||
value = result[index] | ||
while (++index < result.length) { | ||
const value = result[index] | ||
if (object(value) && 'length' in value) { | ||
use.apply(null, value) | ||
// Keep order sequential instead of parallel. | ||
if (Array.isArray(value)) { | ||
const [plugin, primaryValue] = value | ||
await use(plugin, primaryValue) | ||
} else { | ||
use(value) | ||
await use(value, undefined) | ||
} | ||
@@ -196,53 +381,82 @@ } | ||
function addIn(result) { | ||
var key | ||
/** | ||
* @param {PluggableMap} result | ||
* Map of plugins. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function addIn(result) { | ||
/** @type {string} */ | ||
let key | ||
for (key in result) { | ||
use(key, result[key]) | ||
if (own.call(result, key)) { | ||
// Keep order sequential instead of parallel. | ||
await use(key, result[key]) | ||
} | ||
} | ||
} | ||
function use(usable, value) { | ||
if (string(usable)) { | ||
addModule(usable, value) | ||
/** | ||
* @param {PluginSupportingSpecifiers | Preset} usable | ||
* Usable value. | ||
* @param {unknown} value | ||
* Primary parameter. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function use(usable, value) { | ||
if (typeof usable === 'string') { | ||
await addModule(usable, value) | ||
} else if (typeof usable === 'function') { | ||
addPlugin(usable, value) | ||
} else { | ||
merge(target, usable, value, options) | ||
await merge(target, usable, options) | ||
} | ||
} | ||
function addModule(id, value) { | ||
var fp = resolve(id, {cwd: root, prefix: prefix}) | ||
var res | ||
/** | ||
* @param {string} id | ||
* Specifier. | ||
* @param {unknown} value | ||
* Primary parameter. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
async function addModule(id, value) { | ||
/** @type {string} */ | ||
let fp | ||
if (fp) { | ||
try { | ||
res = require(fp) // eslint-disable-line import/no-dynamic-require | ||
} catch (err) { | ||
throw fault( | ||
'Cannot parse script `%s`\n%s', | ||
relative(root, fp), | ||
err.stack | ||
) | ||
} | ||
try { | ||
fp = await resolvePlugin(id, { | ||
cwd: options.root, | ||
prefix: options.prefix | ||
}) | ||
} catch (error) { | ||
addPlugin(function () { | ||
throw new Error('Cannot find module `' + id + '`', {cause: error}) | ||
}, value) | ||
return | ||
} | ||
try { | ||
if (typeof res === 'function') { | ||
addPlugin(res, value) | ||
} else { | ||
merge(target, res, value, xtend(options, {root: dirname(fp)})) | ||
} | ||
} catch (err) { | ||
throw fault( | ||
'Error: Expected preset or plugin, not %s, at `%s`', | ||
res, | ||
relative(root, fp) | ||
) | ||
const result = await loadFromAbsolutePath(fp, options.root) | ||
try { | ||
if (typeof result === 'function') { | ||
// Assume plugin. | ||
const plugin = /** @type {Plugin} */ (result) | ||
addPlugin(plugin, value) | ||
} else { | ||
// Assume preset. | ||
const preset = /** @type {Preset} */ (result) | ||
await merge(target, preset, {...options, root: path.dirname(fp)}) | ||
} | ||
} else { | ||
fp = relative(cwd, path.resolve(root, id)) | ||
addPlugin( | ||
failingModule(fp, new Error('Could not find module `' + id + '`')), | ||
value | ||
} catch (error) { | ||
throw new Error( | ||
'Expected preset or plugin, not `' + | ||
result + | ||
'`, at `' + | ||
path.relative(options.root, fp) + | ||
'`', | ||
{cause: error} | ||
) | ||
@@ -252,9 +466,21 @@ } | ||
function addPlugin(result, value) { | ||
var entry = find(target.plugins, result) | ||
/** | ||
* @param {Plugin} plugin | ||
* Plugin. | ||
* @param {unknown} value | ||
* Primary parameter. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function addPlugin(plugin, value) { | ||
const entry = find(target.plugins, plugin) | ||
if (value === null) { | ||
value = undefined | ||
} | ||
if (entry) { | ||
reconfigure(entry, value) | ||
} else { | ||
target.plugins.push([result, value]) | ||
target.plugins.push([plugin, value]) | ||
} | ||
@@ -264,5 +490,13 @@ } | ||
/** | ||
* @param {PluginTuple} entry | ||
* Tuple. | ||
* @param {unknown} value | ||
* Primary value. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function reconfigure(entry, value) { | ||
if (value !== false && entry[1] !== false && object(value)) { | ||
value = xtend(entry[1], value) | ||
if (isPlainObj(entry[1]) && isPlainObj(value)) { | ||
value = structuredClone({...entry[1], ...value}) | ||
} | ||
@@ -273,10 +507,15 @@ | ||
/** | ||
* @param {Array<PluginTuple>} entries | ||
* Tuples. | ||
* @param {Plugin} plugin | ||
* Plugin. | ||
* @returns {PluginTuple | undefined} | ||
* Tuple. | ||
*/ | ||
function find(entries, plugin) { | ||
var length = entries.length | ||
var index = -1 | ||
var entry | ||
let index = -1 | ||
while (++index < length) { | ||
entry = entries[index] | ||
while (++index < entries.length) { | ||
const entry = entries[index] | ||
if (entry[0] === plugin) { | ||
@@ -288,9 +527,27 @@ return entry | ||
function failingModule(id, err) { | ||
var cache = failingModule.cache || (failingModule.cache = {}) | ||
var submodule = own.call(cache, id) ? cache[id] : (cache[id] = fail) | ||
return submodule | ||
function fail() { | ||
throw err | ||
/** | ||
* @param {string} fp | ||
* Specifier. | ||
* @param {string} base | ||
* Base. | ||
* @returns {Promise<unknown>} | ||
* Result. | ||
*/ | ||
async function loadFromAbsolutePath(fp, base) { | ||
try { | ||
/** @type {ImportResult} */ | ||
const result = await import(pathToFileURL(fp).href) | ||
if (!('default' in result)) { | ||
throw new Error( | ||
'Expected a plugin or preset exported as the default export' | ||
) | ||
} | ||
return result.default | ||
} catch (error) { | ||
throw new Error('Cannot import `' + path.relative(base, fp) + '`', { | ||
cause: error | ||
}) | ||
} | ||
} |
@@ -1,70 +0,93 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var debug = require('debug')('unified-engine:file-pipeline:configure') | ||
var stats = require('vfile-statistics') | ||
var fnName = require('fn-name') | ||
var object = require('is-object') | ||
var empty = require('is-empty') | ||
import createDebug from 'debug' | ||
import isEmpty from 'is-empty' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = configure | ||
const debug = createDebug('unified-engine:file-pipeline:configure') | ||
/* Collect configuration for a file based on the context. */ | ||
function configure(context, file, fileSet, next) { | ||
var config = context.configuration | ||
var processor = context.processor | ||
if (stats(file).fatal) { | ||
return next() | ||
/** | ||
* Collect configuration for a file based on the context. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Callback} next | ||
* Callback | ||
* @returns {undefined} | ||
* Nothing | ||
*/ | ||
export function configure(context, file, next) { | ||
if (statistics(file).fatal || file.data.unifiedEngineIgnored) { | ||
next() | ||
return | ||
} | ||
config.load(file.path, handleConfiguration) | ||
context.configuration.load( | ||
file.path, | ||
/** | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function (error, configuration) { | ||
let index = -1 | ||
function handleConfiguration(err, configuration) { | ||
var plugins | ||
var options | ||
var plugin | ||
var length | ||
var index | ||
var name | ||
if (!configuration) { | ||
next(error) | ||
return | ||
} | ||
if (err) { | ||
return next(err) | ||
} | ||
// If there was no explicit corresponding config file found | ||
if (!configuration.filePath && context.settings.ignoreUnconfigured) { | ||
debug('Ignoring file w/o corresponding config file') | ||
file.data.unifiedEngineIgnored = true | ||
} else { | ||
/* c8 ignore next 1 -- could be missing if a `configTransform` returns weird things. */ | ||
const plugins = configuration.plugins || [] | ||
/* Store configuration on the context object. */ | ||
debug('Using settings `%j`', configuration.settings) | ||
processor.data('settings', configuration.settings) | ||
// Store configuration on the context object. | ||
debug('Using settings `%j`', configuration.settings) | ||
context.processor.data('settings', configuration.settings) | ||
plugins = configuration.plugins | ||
length = plugins.length | ||
index = -1 | ||
debug('Using `%d` plugins', plugins.length) | ||
debug('Using `%d` plugins', length) | ||
while (++index < plugins.length) { | ||
const plugin = plugins[index][0] | ||
let options = plugins[index][1] | ||
while (++index < length) { | ||
plugin = plugins[index][0] | ||
options = plugins[index][1] | ||
if (options === false) { | ||
continue | ||
} | ||
if (options === false) { | ||
continue | ||
} | ||
/* c8 ignore next 6 -- allow for default arguments in es2020. */ | ||
if ( | ||
options === null || | ||
(typeof options === 'object' && isEmpty(options)) | ||
) { | ||
options = undefined | ||
} | ||
/* Allow for default arguments in es2020. */ | ||
if (options === null || (object(options) && empty(options))) { | ||
options = undefined | ||
debug( | ||
'Using plugin `%s`, with options `%j`', | ||
/* c8 ignore next 4 -- V8 is good at inferring names. */ | ||
('displayName' in plugin ? plugin.displayName : 'name') || | ||
plugin.name || | ||
'function', | ||
options | ||
) | ||
context.processor.use(plugin, options, context.fileSet) | ||
} | ||
} | ||
name = fnName(plugin) || 'function' | ||
debug('Using plug-in `%s`, with options `%j`', name, options) | ||
try { | ||
processor.use(plugin, options, fileSet) | ||
} catch (err) { | ||
/* istanbul ignore next - Shouldn’t happen anymore! */ | ||
return next(err) | ||
} | ||
next() | ||
} | ||
next() | ||
} | ||
) | ||
} |
@@ -1,69 +0,88 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var fs = require('fs') | ||
var path = require('path') | ||
var debug = require('debug')('unified-engine:file-pipeline:copy') | ||
var string = require('x-is-string') | ||
import fs from 'node:fs' | ||
import path from 'node:path' | ||
import {fileURLToPath} from 'node:url' | ||
import createDebug from 'debug' | ||
module.exports = copy | ||
const debug = createDebug('unified-engine:file-pipeline:copy') | ||
var stat = fs.stat | ||
var dirname = path.dirname | ||
var resolve = path.resolve | ||
var relative = path.relative | ||
/** | ||
* Move a file. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function copy(context, file, next) { | ||
const output = context.settings.output | ||
const currentPath = file.path | ||
/* Move a file. */ | ||
function copy(context, file, fileSet, next) { | ||
var output = context.output | ||
var multi = fileSet.expected > 1 | ||
var outpath = output | ||
var currentPath = file.path | ||
if (!string(outpath)) { | ||
if ( | ||
output === undefined || | ||
typeof output === 'boolean' || | ||
file.data.unifiedEngineIgnored | ||
) { | ||
debug('Not copying') | ||
return next() | ||
next() | ||
return | ||
} | ||
outpath = resolve(context.cwd, outpath) | ||
const outputPath = typeof output === 'object' ? fileURLToPath(output) : output | ||
const outpath = path.resolve(context.settings.cwd, outputPath) | ||
debug('Copying `%s`', currentPath) | ||
stat(outpath, onstatfile) | ||
function onstatfile(err, stats) { | ||
if (err) { | ||
fs.stat(outpath, function (error, stats) { | ||
if (error) { | ||
if ( | ||
err.code !== 'ENOENT' || | ||
output.charAt(output.length - 1) === path.sep | ||
error.code !== 'ENOENT' || | ||
outputPath.charAt(outputPath.length - 1) === path.sep | ||
) { | ||
return next( | ||
new Error('Cannot read output directory. Error:\n' + err.message) | ||
) | ||
return next(new Error('Cannot read output folder', {cause: error})) | ||
} | ||
stat(dirname(outpath), onstatparent) | ||
// This is either given an error, or the parent exists which is a folder, | ||
// but we should keep the basename of the given file. | ||
fs.stat(path.dirname(outpath), function (error) { | ||
if (error) { | ||
next(new Error('Cannot access parent folder', {cause: error})) | ||
} else { | ||
done(false) | ||
} | ||
}) | ||
} else { | ||
done(stats.isDirectory()) | ||
} | ||
} | ||
}) | ||
/* This is either given an error, or the parent exists which | ||
* is a directory, but we should keep the basename of the | ||
* given file. */ | ||
function onstatparent(err) { | ||
if (err) { | ||
next(new Error('Cannot read parent directory. Error:\n' + err.message)) | ||
} else { | ||
done(false) | ||
} | ||
} | ||
function done(directory) { | ||
if (!directory && multi) { | ||
return next( | ||
new Error('Cannot write multiple files to single output: ' + outpath) | ||
/** | ||
* @param {boolean} folder | ||
* Whether the output is a folder. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function done(folder) { | ||
if (!folder && context.fileSet.expected > 1) { | ||
next( | ||
new Error( | ||
'Cannot write multiple files to single output `' + outpath + '`' | ||
) | ||
) | ||
return | ||
} | ||
file[directory ? 'dirname' : 'path'] = relative(file.cwd, outpath) | ||
file[folder ? 'dirname' : 'path'] = path.relative(file.cwd, outpath) | ||
@@ -70,0 +89,0 @@ debug('Copying document from %s to %s', currentPath, file.path) |
@@ -1,40 +0,61 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var fs = require('fs') | ||
var path = require('path') | ||
var debug = require('debug')('unified-engine:file-pipeline:file-system') | ||
import fs from 'node:fs' | ||
import path from 'node:path' | ||
import createDebug from 'debug' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = fileSystem | ||
const debug = createDebug('unified-engine:file-pipeline:file-system') | ||
var writeFile = fs.writeFile | ||
var resolve = path.resolve | ||
/* Write a virtual file to the file-system. | ||
* Ignored when `output` is not given. */ | ||
function fileSystem(context, file, fileSet, next) { | ||
var destinationPath | ||
if (!context.output) { | ||
/** | ||
* Write a virtual file to the file-system. | ||
* Ignored when `output` is not given. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function fileSystem(context, file, next) { | ||
if (!context.settings.output) { | ||
debug('Ignoring writing to file-system') | ||
return next() | ||
next() | ||
return | ||
} | ||
if (!file.data.unifiedEngineGiven) { | ||
debug('Ignoring programmatically added file') | ||
return next() | ||
if (!file.data.unifiedEngineGiven || file.data.unifiedEngineIgnored) { | ||
debug('Ignoring programmatically added or ignored file') | ||
next() | ||
return | ||
} | ||
destinationPath = file.path | ||
let destinationPath = file.path | ||
if (!destinationPath) { | ||
debug('Cannot write file without a `destinationPath`') | ||
return next(new Error('Cannot write file without an output path ')) | ||
next(new Error('Cannot write file without an output path')) | ||
return | ||
} | ||
destinationPath = resolve(context.cwd, destinationPath) | ||
if (statistics(file).fatal) { | ||
debug('Cannot write file with a fatal error') | ||
next() | ||
return | ||
} | ||
destinationPath = path.resolve(context.settings.cwd, destinationPath) | ||
debug('Writing document to `%s`', destinationPath) | ||
file.stored = true | ||
writeFile(destinationPath, file.toString(), next) | ||
fs.writeFile(destinationPath, file.toString(), next) | ||
} |
@@ -1,66 +0,110 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Pipeline} Pipeline | ||
* | ||
* @typedef {import('unified').Processor} Processor | ||
* | ||
* @typedef {import('unist').Node} Node | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('vfile-message').VFileMessage} VFileMessage | ||
* | ||
* @typedef {import('../configuration.js').Configuration} Configuration | ||
* @typedef {import('../file-set.js').FileSet} FileSet | ||
* @typedef {import('../index.js').Settings} Settings | ||
*/ | ||
var trough = require('trough') | ||
var read = require('./read') | ||
var configure = require('./configure') | ||
var parse = require('./parse') | ||
var transform = require('./transform') | ||
var queue = require('./queue') | ||
var stringify = require('./stringify') | ||
var copy = require('./copy') | ||
var stdout = require('./stdout') | ||
var fileSystem = require('./file-system') | ||
/** | ||
* @typedef Context | ||
* Context. | ||
* @property {Configuration} configuration | ||
* Configuration. | ||
* @property {FileSet} fileSet | ||
* File set. | ||
* @property {Processor} processor | ||
* Processor. | ||
* @property {Settings} settings | ||
* Settings. | ||
* @property {Node | undefined} [tree] | ||
* Tree. | ||
* | ||
* @callback Next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
/* Expose: This pipeline ensures each of the pipes | ||
* always runs: even if the read pipe fails, | ||
* queue and write trigger. */ | ||
module.exports = trough() | ||
.use( | ||
chunk( | ||
trough() | ||
.use(read) | ||
.use(configure) | ||
.use(parse) | ||
.use(transform) | ||
) | ||
) | ||
import {trough} from 'trough' | ||
import {configure} from './configure.js' | ||
import {copy} from './copy.js' | ||
import {fileSystem} from './file-system.js' | ||
import {parse} from './parse.js' | ||
import {queue} from './queue.js' | ||
import {read} from './read.js' | ||
import {stdout} from './stdout.js' | ||
import {stringify} from './stringify.js' | ||
import {transform} from './transform.js' | ||
// This pipeline ensures each of the pipes always runs: even if the read pipe | ||
// fails, queue and write run. | ||
export const filePipeline = trough() | ||
.use(chunk(trough().use(configure).use(read).use(parse).use(transform))) | ||
.use(chunk(trough().use(queue))) | ||
.use( | ||
chunk( | ||
trough() | ||
.use(stringify) | ||
.use(copy) | ||
.use(stdout) | ||
.use(fileSystem) | ||
) | ||
) | ||
.use(chunk(trough().use(stringify).use(copy).use(stdout).use(fileSystem))) | ||
/* Factory to run a pipe. Wraps a pipe to trigger an | ||
* error on the `file` in `context`, but still call | ||
* `next`. */ | ||
/** | ||
* Factory to run a pipe. | ||
* Wraps a pipe to trigger an error on the `file` in `context`, but still call | ||
* `next`. | ||
* | ||
* @param {Pipeline} pipe | ||
* Pipe. | ||
* @returns | ||
* Run function. | ||
*/ | ||
function chunk(pipe) { | ||
return run | ||
/* Run the bound bound pipe and handles any errors. */ | ||
function run(context, file, fileSet, next) { | ||
pipe.run(context, file, fileSet, one) | ||
/** | ||
* Run the bound pipe and handle any errors. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Next} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function run(context, file, next) { | ||
pipe.run( | ||
context, | ||
file, | ||
/** | ||
* @param {VFileMessage | undefined} error | ||
* Error. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function (error) { | ||
const messages = file.messages | ||
function one(err) { | ||
var messages = file.messages | ||
var index | ||
if (error) { | ||
const index = messages.indexOf(error) | ||
if (err) { | ||
index = messages.indexOf(err) | ||
if (index === -1) { | ||
err = file.message(err) | ||
index = messages.length - 1 | ||
if (index === -1) { | ||
const message = file.message('Cannot process file', { | ||
cause: error | ||
}) | ||
message.fatal = true | ||
} else { | ||
messages[index].fatal = true | ||
} | ||
} | ||
messages[index].fatal = true | ||
next() | ||
} | ||
next() | ||
} | ||
) | ||
} | ||
} |
@@ -1,37 +0,53 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('unist').Node} Node | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var debug = require('debug')('unified-engine:file-pipeline:parse') | ||
var stats = require('vfile-statistics') | ||
var json = require('parse-json') | ||
import createDebug from 'debug' | ||
import parseJson from 'parse-json' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = parse | ||
const debug = createDebug('unified-engine:file-pipeline:parse') | ||
/* Fill a file with a tree. */ | ||
function parse(context, file) { | ||
var message | ||
if (stats(file).fatal) { | ||
/** | ||
* Fill a file with a tree. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function parse(context, file) { | ||
if (statistics(file).fatal || file.data.unifiedEngineIgnored) { | ||
return | ||
} | ||
if (context.treeIn) { | ||
if (context.settings.treeIn) { | ||
debug('Not parsing already parsed document') | ||
try { | ||
context.tree = json(file.toString()) | ||
} catch (err) { | ||
message = file.message( | ||
new Error('Cannot read file as JSON\n' + err.message) | ||
// Assume it’s a valid node. | ||
const tree = /** @type {Node} */ ( | ||
/** @type {unknown} */ (parseJson(file.toString())) | ||
) | ||
context.tree = tree | ||
} catch (error) { | ||
const cause = /** @type {Error} */ (error) | ||
const message = file.message('Cannot read file as JSON', {cause}) | ||
message.fatal = true | ||
} | ||
/* Add the preferred extension to ensure the file, when compiled, is | ||
* correctly recognized. Only add it if there’s a path — not if the | ||
* file is for example stdin. */ | ||
// Add the preferred extension to ensure the file, when serialized, is | ||
// correctly recognised. | ||
// Only add it if there is a path — not if the file is for example stdin. | ||
if (file.path) { | ||
file.extname = context.extensions[0] | ||
file.extname = context.settings.extensions[0] | ||
} | ||
file.contents = '' | ||
file.value = '' | ||
@@ -38,0 +54,0 @@ return |
@@ -1,20 +0,33 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var debug = require('debug')('unified-engine:file-pipeline:queue') | ||
var stats = require('vfile-statistics') | ||
import createDebug from 'debug' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = queue | ||
const debug = createDebug('unified-engine:file-pipeline:queue') | ||
/* Queue all files which came this far. | ||
* When the last file gets here, run the file-set pipeline | ||
* and flush the queue. */ | ||
function queue(context, file, fileSet, next) { | ||
var origin = file.history[0] | ||
var map = fileSet.complete | ||
var complete = true | ||
const own = {}.hasOwnProperty | ||
if (!map) { | ||
map = {} | ||
fileSet.complete = map | ||
} | ||
/** | ||
* Queue all files which came this far. | ||
* When the last file gets here, run the file-set pipeline and flush the queue. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function queue(context, file, next) { | ||
let origin = file.history[0] | ||
const map = context.fileSet.complete | ||
let complete = true | ||
@@ -25,3 +38,7 @@ debug('Queueing `%s`', origin) | ||
fileSet.valueOf().forEach(each) | ||
const files = context.fileSet.valueOf() | ||
let index = -1 | ||
while (++index < files.length) { | ||
each(files[index]) | ||
} | ||
@@ -33,10 +50,15 @@ if (!complete) { | ||
fileSet.complete = {} | ||
context.fileSet.complete = {} | ||
context.fileSet.pipeline.run(context.fileSet, done) | ||
fileSet.pipeline.run(fileSet, done) | ||
/** | ||
* @param {VFile} file | ||
* File. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function each(file) { | ||
var key = file.history[0] | ||
const key = file.history[0] | ||
if (stats(file).fatal) { | ||
if (statistics(file).fatal || file.data.unifiedEngineIgnored) { | ||
return | ||
@@ -53,10 +75,18 @@ } | ||
function done(err) { | ||
/** | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function done(error) { | ||
debug('Flushing: all files can be flushed') | ||
/* Flush. */ | ||
// Flush. | ||
for (origin in map) { | ||
map[origin](err) | ||
if (own.call(map, origin)) { | ||
map[origin](error) | ||
} | ||
} | ||
} | ||
} |
@@ -1,37 +0,52 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var fs = require('fs') | ||
var path = require('path') | ||
var debug = require('debug')('unified-engine:file-pipeline:read') | ||
var stats = require('vfile-statistics') | ||
import fs from 'node:fs' | ||
import path from 'node:path' | ||
import createDebug from 'debug' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = read | ||
const debug = createDebug('unified-engine:file-pipeline:read') | ||
var resolve = path.resolve | ||
var readFile = fs.readFile | ||
/** | ||
* Fill a file with its value when not already filled. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function read(context, file, next) { | ||
let filePath = file.path | ||
/* Fill a file with its contents when not already filled. */ | ||
function read(context, file, fileSet, next) { | ||
var filePath = file.path | ||
if (file.contents || file.data.unifiedEngineStreamIn) { | ||
debug('Not reading file `%s` with contents', filePath) | ||
if ( | ||
(file.value !== null && file.value !== undefined) || | ||
file.data.unifiedEngineStreamIn | ||
) { | ||
debug('Not reading file `%s` with `value`', filePath) | ||
next() | ||
} else if (stats(file).fatal) { | ||
debug('Not reading failed file `%s`', filePath) | ||
} else if (statistics(file).fatal || file.data.unifiedEngineIgnored) { | ||
debug('Not reading failed or ignored file `%s`', filePath) | ||
next() | ||
} else { | ||
filePath = resolve(context.cwd, filePath) | ||
filePath = path.resolve(context.settings.cwd, filePath) | ||
debug('Reading `%s` in `%s`', filePath, 'utf8') | ||
readFile(filePath, 'utf8', onread) | ||
} | ||
fs.readFile(filePath, 'utf8', function (error, value) { | ||
debug('Read `%s` (error: %s)', filePath, error) | ||
function onread(err, contents) { | ||
debug('Read `%s` (err: %s)', filePath, err) | ||
file.value = value || '' | ||
file.contents = contents || '' | ||
next(err) | ||
next(error) | ||
}) | ||
} | ||
} |
@@ -1,16 +0,38 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var debug = require('debug')('unified-engine:file-pipeline:stdout') | ||
var stats = require('vfile-statistics') | ||
import createDebug from 'debug' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = stdout | ||
const debug = createDebug('unified-engine:file-pipeline:stdout') | ||
/* Write a virtual file to `streamOut`. | ||
* Ignored when `output` is given, more than one file | ||
* was processed, or `out` is false. */ | ||
function stdout(context, file, fileSet, next) { | ||
/** | ||
* Write a virtual file to `streamOut`. | ||
* Ignored when `output` is given, more than one file was processed, or `out` | ||
* is false. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function stdout(context, file, next) { | ||
if (!file.data.unifiedEngineGiven) { | ||
debug('Ignoring programmatically added file') | ||
next() | ||
} else if (stats(file).fatal || context.output || !context.out) { | ||
} else if ( | ||
statistics(file).fatal || | ||
file.data.unifiedEngineIgnored || | ||
context.settings.output || | ||
!context.settings.out | ||
) { | ||
debug('Ignoring writing to `streamOut`') | ||
@@ -20,4 +42,4 @@ next() | ||
debug('Writing document to `streamOut`') | ||
context.streamOut.write(file.toString(), next) | ||
context.settings.streamOut.write(file.toString(), next) | ||
} | ||
} |
@@ -1,21 +0,38 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var debug = require('debug')('unified-engine:file-pipeline:stringify') | ||
var stats = require('vfile-statistics') | ||
var inspect = require('unist-util-inspect') | ||
import assert from 'node:assert/strict' | ||
import createDebug from 'debug' | ||
import {inspectColor, inspectNoColor} from 'unist-util-inspect' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = stringify | ||
const debug = createDebug('unified-engine:file-pipeline:stringify') | ||
/* Stringify a tree. */ | ||
function stringify(context, file) { | ||
var processor = context.processor | ||
var tree = context.tree | ||
var value | ||
/** | ||
* Stringify a tree. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function stringify(context, file) { | ||
/** @type {unknown} */ | ||
let value | ||
if (stats(file).fatal) { | ||
debug('Not compiling failed document') | ||
if (statistics(file).fatal || file.data.unifiedEngineIgnored) { | ||
debug('Not compiling failed or ignored document') | ||
return | ||
} | ||
if (!context.output && !context.out && !context.alwaysStringify) { | ||
if ( | ||
!context.settings.output && | ||
!context.settings.out && | ||
!context.settings.alwaysStringify | ||
) { | ||
debug('Not compiling document without output settings') | ||
@@ -27,4 +44,4 @@ return | ||
if (context.inspect) { | ||
/* Add a `txt` extension if there’s a path. */ | ||
if (context.settings.inspect) { | ||
// Add a `txt` extension if there is a path. | ||
if (file.path) { | ||
@@ -34,6 +51,8 @@ file.extname = '.txt' | ||
value = inspect[context.color ? 'color' : 'noColor'](tree) + '\n' | ||
} else if (context.treeOut) { | ||
/* Add a `json` extension to ensure the file is correctly seen as JSON. | ||
* Only add it if there’s a path — not if the file is for example stdin. */ | ||
value = | ||
(context.settings.color ? inspectColor : inspectNoColor)(context.tree) + | ||
'\n' | ||
} else if (context.settings.treeOut) { | ||
// Add a `json` extension to ensure the file is correctly seen as JSON. | ||
// Only add it if there is a path — not if the file is for example stdin. | ||
if (file.path) { | ||
@@ -43,11 +62,35 @@ file.extname = '.json' | ||
/* Add the line break to create a valid UNIX file. */ | ||
value = JSON.stringify(tree, null, 2) + '\n' | ||
// Add the line feed to create a valid UNIX file. | ||
value = JSON.stringify(context.tree, undefined, 2) + '\n' | ||
} else { | ||
value = processor.stringify(tree, file) | ||
assert(context.tree, '`tree` is defined if we came this far') | ||
value = context.processor.stringify(context.tree, file) | ||
} | ||
file.contents = value | ||
if (value === null || value === undefined) { | ||
// Empty. | ||
} else if (typeof value === 'string' || isUint8Array(value)) { | ||
file.value = value | ||
} else { | ||
file.result = value | ||
} | ||
debug('Compiled document') | ||
debug('Serialized document') | ||
} | ||
/** | ||
* Assert `value` is an `Uint8Array`. | ||
* | ||
* @param {unknown} value | ||
* Thing. | ||
* @returns {value is Uint8Array} | ||
* Whether `value` is an `Uint8Array`. | ||
*/ | ||
function isUint8Array(value) { | ||
return Boolean( | ||
value && | ||
typeof value === 'object' && | ||
'byteLength' in value && | ||
'byteOffset' in value | ||
) | ||
} |
@@ -1,23 +0,39 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('./index.js').Context} Context | ||
*/ | ||
var debug = require('debug')('unified-engine:file-pipeline:transform') | ||
var stats = require('vfile-statistics') | ||
import assert from 'node:assert/strict' | ||
import createDebug from 'debug' | ||
import {statistics} from 'vfile-statistics' | ||
module.exports = transform | ||
const debug = createDebug('unified-engine:file-pipeline:transform') | ||
/* Transform the tree associated with a file with | ||
* configured plug-ins. */ | ||
function transform(context, file, fileSet, next) { | ||
if (stats(file).fatal) { | ||
/** | ||
* Transform the tree associated with a file with configured plugins. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {VFile} file | ||
* File. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function transform(context, file, next) { | ||
if (statistics(file).fatal || file.data.unifiedEngineIgnored) { | ||
next() | ||
} else { | ||
assert(context.tree, '`tree` is defined at this point') | ||
debug('Transforming document `%s`', file.path) | ||
context.processor.run(context.tree, file, onrun) | ||
context.processor.run(context.tree, file, function (error, node) { | ||
debug('Transformed document (error: %s)', error) | ||
context.tree = node | ||
next(error) | ||
}) | ||
} | ||
function onrun(err, node) { | ||
debug('Transformed document (error: %s)', err) | ||
context.tree = node | ||
next(err) | ||
} | ||
} |
@@ -1,9 +0,24 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('../index.js').Settings} Settings | ||
*/ | ||
var Configuration = require('../configuration') | ||
/** | ||
* @typedef Context | ||
* Context. | ||
* @property {Configuration | undefined} [configuration] | ||
* Configuration. | ||
*/ | ||
module.exports = configure | ||
import {Configuration} from '../configuration.js' | ||
function configure(context, settings) { | ||
/** | ||
* @param {Context} context | ||
* Context. | ||
* @param {Settings} settings | ||
* Settings. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function configure(context, settings) { | ||
context.configuration = new Configuration(settings) | ||
} |
@@ -1,21 +0,41 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('../configuration.js').Configuration} Configuration | ||
* @typedef {import('../index.js').Settings} Settings | ||
*/ | ||
var Ignore = require('../ignore') | ||
var find = require('../finder') | ||
/** | ||
* @typedef Context | ||
* Context. | ||
* @property {Array<VFile | string>} files | ||
* Files. | ||
* @property {Configuration | undefined} [configuration] | ||
* Configuration. | ||
*/ | ||
module.exports = fileSystem | ||
import {finder} from '../finder.js' | ||
import {Ignore} from '../ignore.js' | ||
/* Find files from the file-system. */ | ||
function fileSystem(context, settings, next) { | ||
var input = context.files | ||
if (input.length === 0) { | ||
/** | ||
* @param {Context} context | ||
* Context. | ||
* @param {Settings} settings | ||
* Settings. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function fileSystem(context, settings, next) { | ||
if (context.files.length === 0) { | ||
next() | ||
} else { | ||
find( | ||
input, | ||
finder( | ||
context.files, | ||
{ | ||
cwd: settings.cwd, | ||
extensions: settings.extensions, | ||
silentlyIgnore: settings.silentlyIgnore, | ||
ignore: new Ignore({ | ||
@@ -25,35 +45,50 @@ cwd: settings.cwd, | ||
ignoreName: settings.ignoreName, | ||
ignorePath: settings.ignorePath | ||
}) | ||
ignorePath: settings.ignorePath, | ||
ignorePathResolveFrom: settings.ignorePathResolveFrom | ||
}), | ||
ignorePatterns: settings.ignorePatterns, | ||
silentlyIgnore: settings.silentlyIgnore | ||
}, | ||
onfound | ||
) | ||
} | ||
function (error, result) { | ||
/* c8 ignore next 4 -- glob errors are unusual. */ | ||
if (!result) { | ||
next(error) | ||
return | ||
} | ||
function onfound(err, result) { | ||
var output = result.files | ||
const output = result.files | ||
/* Sort alphabetically. Everything’s unique so we don’t care | ||
* about cases where left and right are equal. */ | ||
output.sort(sortAlphabetically) | ||
// Sort alphabetically. | ||
// Everything is unique so we do not care about cases where left and right | ||
// are equal. | ||
output.sort(sortAlphabetically) | ||
/* Mark as given. This allows outputting files, | ||
* which can be pretty dangerous, so it’s “hidden”. */ | ||
output.forEach(markAsGiven) | ||
// Mark as given. | ||
// This allows outputting files, which can be pretty dangerous, so it’s | ||
// “hidden”. | ||
let index = -1 | ||
while (++index < output.length) { | ||
output[index].data.unifiedEngineGiven = true | ||
} | ||
context.files = output | ||
context.files = output | ||
/* If `out` wasn’t set, detect it based on | ||
* whether one file was given. */ | ||
if (settings.out === null || settings.out === undefined) { | ||
settings.out = result.oneFileMode | ||
} | ||
// If `out` was not set, detect it based on whether one file was given. | ||
if (settings.out === undefined) { | ||
settings.out = result.oneFileMode | ||
} | ||
next(err) | ||
next(error) | ||
} | ||
) | ||
} | ||
function markAsGiven(file) { | ||
file.data.unifiedEngineGiven = true | ||
} | ||
/** | ||
* @param {VFile} left | ||
* File. | ||
* @param {VFile} right | ||
* Other file. | ||
* @returns {number} | ||
* Order. | ||
*/ | ||
function sortAlphabetically(left, right) { | ||
@@ -60,0 +95,0 @@ return left.path < right.path ? -1 : 1 |
@@ -1,11 +0,9 @@ | ||
'use strict' | ||
import {trough} from 'trough' | ||
import {configure} from './configure.js' | ||
import {fileSystem} from './file-system.js' | ||
import {log} from './log.js' | ||
import {stdin} from './stdin.js' | ||
import {transform} from './transform.js' | ||
var trough = require('trough') | ||
var configure = require('./configure') | ||
var fileSystem = require('./file-system') | ||
var stdin = require('./stdin') | ||
var transform = require('./transform') | ||
var log = require('./log') | ||
module.exports = trough() | ||
export const fileSetPipeline = trough() | ||
.use(configure) | ||
@@ -12,0 +10,0 @@ .use(fileSystem) |
@@ -1,32 +0,63 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('../configuration.js').Configuration} Configuration | ||
* @typedef {import('../index.js').Settings} Settings | ||
* @typedef {import('../index.js').VFileReporter} VFileReporter | ||
*/ | ||
var xtend = require('xtend') | ||
var load = require('load-plugin') | ||
var report = require('vfile-reporter') | ||
var string = require('x-is-string') | ||
import {loadPlugin} from 'load-plugin' | ||
import {reporter} from 'vfile-reporter' | ||
module.exports = log | ||
/** | ||
* @typedef Context | ||
* Context. | ||
* @property {Array<VFile>} files | ||
* Files. | ||
* @property {Configuration | undefined} [configuration] | ||
* Configuration. | ||
*/ | ||
var prefix = 'vfile-reporter' | ||
/** | ||
* @param {Context} context | ||
* Context. | ||
* @param {Settings} settings | ||
* Settings. | ||
* @returns {Promise<undefined>} | ||
* Nothing. | ||
*/ | ||
export async function log(context, settings) { | ||
/** @type {VFileReporter} */ | ||
let func = reporter | ||
function log(context, settings, next) { | ||
var reporter = settings.reporter || report | ||
var diagnostics | ||
if (typeof settings.reporter === 'string') { | ||
try { | ||
// Assume a valid reporter. | ||
const result = /** @type {VFileReporter} */ ( | ||
await loadPlugin(settings.reporter, { | ||
cwd: settings.cwd, | ||
prefix: 'vfile-reporter' | ||
}) | ||
) | ||
if (string(reporter)) { | ||
try { | ||
reporter = load(reporter, {cwd: settings.cwd, prefix: prefix}) | ||
} catch (err) { | ||
next(new Error('Could not find reporter `' + reporter + '`')) | ||
return | ||
func = result | ||
} catch (error) { | ||
throw new Error('Cannot find reporter `' + settings.reporter + '`', { | ||
cause: error | ||
}) | ||
} | ||
} else if (settings.reporter) { | ||
func = settings.reporter | ||
} | ||
diagnostics = reporter( | ||
context.files.filter(given), | ||
xtend(settings.reporterOptions, { | ||
let diagnostics = await func( | ||
context.files.filter(function (file) { | ||
return file.data.unifiedEngineGiven && !file.data.unifiedEngineIgnored | ||
}), | ||
{ | ||
...settings.reporterOptions, | ||
color: settings.color, | ||
quiet: settings.quiet, | ||
silent: settings.silent, | ||
color: settings.color | ||
}) | ||
silent: settings.silent | ||
} | ||
) | ||
@@ -39,10 +70,6 @@ | ||
settings.streamError.write(diagnostics, next) | ||
} else { | ||
next() | ||
return new Promise(function (resolve) { | ||
settings.streamError.write(diagnostics, () => resolve(undefined)) | ||
}) | ||
} | ||
} | ||
function given(file) { | ||
return file.data.unifiedEngineGiven | ||
} |
@@ -1,23 +0,44 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('../index.js').Settings} Settings | ||
*/ | ||
var debug = require('debug')('unified-engine:file-set-pipeline:stdin') | ||
var vfile = require('to-vfile') | ||
var concat = require('concat-stream') | ||
/** | ||
* @typedef Context | ||
* Context. | ||
* @property {Array<VFile | string>} files | ||
* Files. | ||
*/ | ||
module.exports = stdin | ||
import concatStream from 'concat-stream' | ||
import createDebug from 'debug' | ||
import {VFile} from 'vfile' | ||
function stdin(context, settings, next) { | ||
var streamIn = settings.streamIn | ||
var err | ||
const debug = createDebug('unified-engine:file-set-pipeline:stdin') | ||
if (settings.files && settings.files.length !== 0) { | ||
/** | ||
* @param {Context} context | ||
* Context. | ||
* @param {Settings} settings | ||
* Settings. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function stdin(context, settings, next) { | ||
if (settings.files && settings.files.length > 0) { | ||
debug('Ignoring `streamIn`') | ||
/** @type {Error | undefined} */ | ||
let error | ||
if (settings.filePath) { | ||
err = new Error( | ||
'Do not pass both `--file-path` and real files.\nDid you mean to pass stdin instead of files?' | ||
error = new Error( | ||
'Do not pass both `filePath` and real files.\nDid you mean to pass stdin instead of files?' | ||
) | ||
} | ||
next(err) | ||
next(error) | ||
@@ -27,3 +48,3 @@ return | ||
if (streamIn.isTTY) { | ||
if ('isTTY' in settings.streamIn && settings.streamIn.isTTY) { | ||
debug('Cannot read from `tty` stream') | ||
@@ -37,22 +58,21 @@ next(new Error('No input')) | ||
streamIn.pipe(concat({encoding: 'string'}, read)) | ||
settings.streamIn.pipe( | ||
concatStream({encoding: 'string'}, function (value) { | ||
const file = new VFile({path: settings.filePath}) | ||
function read(value) { | ||
var file = vfile(settings.filePath || undefined) | ||
debug('Read from `streamIn`') | ||
debug('Read from `streamIn`') | ||
file.cwd = settings.cwd | ||
file.value = value | ||
file.data.unifiedEngineGiven = true | ||
file.data.unifiedEngineStreamIn = true | ||
file.cwd = settings.cwd | ||
file.contents = value | ||
file.data.unifiedEngineGiven = true | ||
file.data.unifiedEngineStreamIn = true | ||
context.files = [file] | ||
context.files = [file] | ||
// If `out` was not set, set `out`. | ||
settings.out = settings.out === undefined ? true : settings.out | ||
/* If `out` wasn’t set, set `out`. */ | ||
settings.out = | ||
settings.out === null || settings.out === undefined ? true : settings.out | ||
next() | ||
} | ||
next() | ||
}) | ||
) | ||
} |
@@ -1,57 +0,82 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Callback} Callback | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('../configuration.js').Configuration} Configuration | ||
* @typedef {import('../index.js').Settings} Settings | ||
*/ | ||
var FileSet = require('../file-set') | ||
var filePipeline = require('../file-pipeline') | ||
/** | ||
* @typedef Context | ||
* Context. | ||
* @property {Array<VFile>} files | ||
* Files. | ||
* @property {Configuration} configuration | ||
* Configuration. | ||
* @property {FileSet} fileSet | ||
* File set. | ||
*/ | ||
module.exports = transform | ||
import {filePipeline} from '../file-pipeline/index.js' | ||
import {FileSet} from '../file-set.js' | ||
/* Transform all files. */ | ||
function transform(context, settings, next) { | ||
var fileSet = new FileSet() | ||
/** | ||
* Transform all files. | ||
* | ||
* @param {Context} context | ||
* Context. | ||
* @param {Settings} settings | ||
* Settings. | ||
* @param {Callback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function transform(context, settings, next) { | ||
const fileSet = new FileSet() | ||
context.fileSet = fileSet | ||
fileSet.on('add', add).on('done', next) | ||
if (context.files.length === 0) { | ||
next() | ||
} else { | ||
context.files.forEach(fileSet.add, fileSet) | ||
} | ||
function add(file) { | ||
fileSet.on('add', function (/** @type {VFile} */ file) { | ||
filePipeline.run( | ||
{ | ||
configuration: context.configuration, | ||
fileSet, | ||
// Needed `any`s | ||
// type-coverage:ignore-next-line | ||
processor: settings.processor(), | ||
cwd: settings.cwd, | ||
extensions: settings.extensions, | ||
pluginPrefix: settings.pluginPrefix, | ||
treeIn: settings.treeIn, | ||
treeOut: settings.treeOut, | ||
inspect: settings.inspect, | ||
color: settings.color, | ||
out: settings.out, | ||
output: settings.output, | ||
streamOut: settings.streamOut, | ||
alwaysStringify: settings.alwaysStringify | ||
settings | ||
}, | ||
file, | ||
fileSet, | ||
done | ||
) | ||
/** | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function (error) { | ||
// Does not occur, all failures in `filePipeLine` are failed on each | ||
// file. | ||
/* c8 ignore next 4 -- extra handling that currently isn’t used. */ | ||
if (error) { | ||
const message = file.message('Cannot transform file', {cause: error}) | ||
message.fatal = true | ||
} | ||
function done(err) { | ||
/* istanbul ignore next - doesn’t occur as all | ||
* failures in `filePipeLine` are failed on each | ||
* file. Still, just to ensure things work in | ||
* the future, we add an extra check. */ | ||
if (err) { | ||
err = file.message(err) | ||
err.fatal = true | ||
fileSet.emit('one', file) | ||
} | ||
) | ||
}) | ||
fileSet.emit('one', file) | ||
fileSet.on('done', next) | ||
if (context.files.length === 0) { | ||
next() | ||
} else { | ||
let index = -1 | ||
while (++index < context.files.length) { | ||
fileSet.add(context.files[index]) | ||
} | ||
} | ||
} |
@@ -1,120 +0,188 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('trough').Pipeline} Pipeline | ||
*/ | ||
var events = require('events') | ||
var inherits = require('util').inherits | ||
var trough = require('trough') | ||
var vfile = require('to-vfile') | ||
var string = require('x-is-string') | ||
/** | ||
* @typedef {(CompleterCallback | CompleterRegular) & {pluginId?: string | symbol | undefined}} Completer | ||
* Completer. | ||
* | ||
* @callback CompleterCallback | ||
* Handle a set having processed, in callback-style. | ||
* @param {FileSet} set | ||
* File set. | ||
* @param {CompleterCallbackNext} next | ||
* Callback called when done. | ||
* @returns {undefined | void} | ||
* Result. | ||
* | ||
* Note: `void` included because TS sometimes infers it. | ||
* | ||
* @callback CompleterCallbackNext | ||
* Callback called when done. | ||
* @param {Error | null | undefined} [error] | ||
* Error. | ||
* @returns {undefined} | ||
* Nothing. | ||
* | ||
* @callback CompleterRegular | ||
* Handle a set having processed. | ||
* @param {FileSet} set | ||
* File set. | ||
* @returns {Promise<undefined> | undefined | void} | ||
* Nothing. | ||
* | ||
* Note: `void` included because TS sometimes infers it. | ||
*/ | ||
module.exports = FileSet | ||
import {EventEmitter} from 'node:events' | ||
import {trough} from 'trough' | ||
import {VFile} from 'vfile' | ||
/* FileSet constructor. */ | ||
function FileSet() { | ||
var self = this | ||
export class FileSet extends EventEmitter { | ||
/** | ||
* FileSet. | ||
* | ||
* A FileSet is created to process multiple files through unified processors. | ||
* This set, containing all files, is exposed to plugins as an argument to the | ||
* attacher. | ||
*/ | ||
constructor() { | ||
super() | ||
self.files = [] | ||
self.origins = [] | ||
const self = this | ||
self.expected = 0 | ||
self.actual = 0 | ||
/** | ||
* @deprecated | ||
* Internal field that should be considered private. | ||
* @type {number} | ||
*/ | ||
this.actual = 0 | ||
/** | ||
* This is used by the `queue` to stash async work. | ||
* | ||
* @deprecated | ||
* Internal field that should be considered private. | ||
* @type {Record<string, Function>} | ||
*/ | ||
this.complete = {} | ||
/** | ||
* @deprecated | ||
* Internal field that should be considered private. | ||
* @type {number} | ||
*/ | ||
this.expected = 0 | ||
/** | ||
* @deprecated | ||
* Internal field that should be considered private. | ||
* @type {Array<VFile>} | ||
*/ | ||
this.files = [] | ||
/** | ||
* @deprecated | ||
* Internal field that should be considered private. | ||
* @type {Array<string>} | ||
*/ | ||
this.origins = [] | ||
/** | ||
* @deprecated | ||
* Internal field that should be considered private. | ||
* @type {Pipeline} | ||
*/ | ||
this.pipeline = trough() | ||
/** | ||
* @deprecated | ||
* Internal field that should be considered private. | ||
* @type {Array<Completer>} | ||
*/ | ||
this.plugins = [] | ||
self.pipeline = trough() | ||
self.plugins = [] | ||
// Called when a single file has completed it’s pipeline, triggering `done` | ||
// when all files are complete. | ||
this.on('one', function () { | ||
self.actual++ | ||
events.init.call(self) | ||
self.on('one', one.bind(self)) | ||
} | ||
/* Events. */ | ||
inherits(FileSet, events.EventEmitter) | ||
/* Expose methods. */ | ||
FileSet.prototype.valueOf = valueOf | ||
FileSet.prototype.use = use | ||
FileSet.prototype.add = add | ||
/* Create an array representation of `fileSet`. */ | ||
function valueOf() { | ||
return this.files | ||
} | ||
/* Attach middleware to the pipeline on `fileSet`. */ | ||
function use(plugin) { | ||
var self = this | ||
var pipeline = self.pipeline | ||
var duplicate = false | ||
if (plugin && plugin.pluginId) { | ||
duplicate = self.plugins.some(matches) | ||
if (self.actual >= self.expected) { | ||
self.emit('done') | ||
} | ||
}) | ||
} | ||
if (!duplicate && self.plugins.indexOf(plugin) !== -1) { | ||
duplicate = true | ||
/** | ||
* Get files in a set. | ||
*/ | ||
valueOf() { | ||
return this.files | ||
} | ||
if (!duplicate) { | ||
self.plugins.push(plugin) | ||
pipeline.use(plugin) | ||
} | ||
/** | ||
* Add middleware to be called when done. | ||
* | ||
* @param {Completer} completer | ||
* Plugin. | ||
* @returns | ||
* Self. | ||
*/ | ||
use(completer) { | ||
const pipeline = this.pipeline | ||
let duplicate = false | ||
return self | ||
if (completer && completer.pluginId) { | ||
duplicate = this.plugins.some(function (fn) { | ||
return fn.pluginId === completer.pluginId | ||
}) | ||
} | ||
function matches(fn) { | ||
return fn.pluginId === plugin.pluginId | ||
} | ||
} | ||
if (!duplicate && this.plugins.includes(completer)) { | ||
duplicate = true | ||
} | ||
/* Add a file to be processed. | ||
* | ||
* Ignores duplicate files (based on the `filePath` at time | ||
* of addition). | ||
* | ||
* Only runs `file-pipeline` on files which have not | ||
* `failed` before addition. */ | ||
function add(file) { | ||
var self = this | ||
var origin | ||
if (!duplicate) { | ||
this.plugins.push(completer) | ||
pipeline.use(completer) | ||
} | ||
if (string(file)) { | ||
file = vfile(file) | ||
return this | ||
} | ||
/* Prevent files from being added multiple times. */ | ||
origin = file.history[0] | ||
/** | ||
* Add a file. | ||
* | ||
* The given file is processed like other files with a few differences: | ||
* | ||
* * Ignored when their file path is already added | ||
* * Never written to the file system or `streamOut` | ||
* * Not included in the report | ||
* | ||
* @param {VFile | string} file | ||
* File or file path. | ||
* @returns | ||
* Self. | ||
*/ | ||
add(file) { | ||
const self = this | ||
if (self.origins.indexOf(origin) !== -1) { | ||
return self | ||
} | ||
if (typeof file === 'string') { | ||
file = new VFile({path: file}) | ||
} | ||
self.origins.push(origin) | ||
// Prevent files from being added multiple times. | ||
if (this.origins.includes(file.history[0])) { | ||
return this | ||
} | ||
/* Add. */ | ||
self.valueOf().push(file) | ||
self.expected++ | ||
this.origins.push(file.history[0]) | ||
/* Force an asynchronous operation. | ||
* This ensures that files which fall through | ||
* the file pipeline immediately (e.g., when | ||
* already fatally failed) still queue up | ||
* correctly. */ | ||
setImmediate(add) | ||
// Add. | ||
this.valueOf().push(file) | ||
this.expected++ | ||
return self | ||
// Force an asynchronous operation. | ||
// This ensures that files which fall through the file pipeline immediately | ||
// (such as, when already fatally failed) still queue up correctly. | ||
setImmediate(function () { | ||
self.emit('add', file) | ||
}) | ||
function add() { | ||
self.emit('add', file) | ||
return this | ||
} | ||
} | ||
/* Utility invoked when a single file has completed it's | ||
* pipeline, triggering `done` when all files are complete. */ | ||
function one() { | ||
var self = this | ||
self.actual++ | ||
if (self.actual >= self.expected) { | ||
self.emit('done') | ||
} | ||
} |
@@ -1,188 +0,307 @@ | ||
'use strict' | ||
/** | ||
* @template Value | ||
* Value type. | ||
* @callback Callback | ||
* Callback called when something is found. | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {Value | undefined} [result] | ||
* Value. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
var fs = require('fs') | ||
var path = require('path') | ||
var fault = require('fault') | ||
var debug = require('debug')('unified-engine:find-up') | ||
var object = require('is-object') | ||
/** | ||
* @template Value | ||
* Value type. | ||
* @callback Create | ||
* Transform a file to a certain value. | ||
* @param {Buffer} value | ||
* File contents. | ||
* @param {string} filePath | ||
* File path. | ||
* @returns {Promise<Value | undefined> | Value | undefined} | ||
* Value. | ||
*/ | ||
module.exports = FindUp | ||
/** | ||
* @typedef FindValue | ||
* Bare interface of value. | ||
* @property {string | undefined} filePath | ||
* File path. | ||
*/ | ||
var read = fs.readFile | ||
var resolve = path.resolve | ||
var relative = path.relative | ||
var join = path.join | ||
var dirname = path.dirname | ||
/** | ||
* @template Value | ||
* Value type. | ||
* @typedef Options | ||
* Configuration. | ||
* @property {string} cwd | ||
* Base. | ||
* @property {URL | string | undefined} filePath | ||
* File path of a given file. | ||
* @property {boolean | undefined} [detect=false] | ||
* Whether to detect files (default: `false`). | ||
* @property {Array<string>} names | ||
* Basenames of files to look for. | ||
* @property {Create<Value>} create | ||
* Turn a found file into a value. | ||
*/ | ||
FindUp.prototype.load = load | ||
import assert from 'node:assert/strict' | ||
import fs from 'node:fs' | ||
import path from 'node:path' | ||
import {fileURLToPath} from 'node:url' | ||
import createDebug from 'debug' | ||
import {wrap} from 'trough' | ||
function FindUp(options) { | ||
var self = this | ||
var fp = options.filePath | ||
const debug = createDebug('unified-engine:find-up') | ||
self.cache = {} | ||
self.cwd = options.cwd | ||
self.detect = options.detect | ||
self.names = options.names | ||
self.create = options.create | ||
/** | ||
* @template {FindValue} Value | ||
* Value to find. | ||
*/ | ||
export class FindUp { | ||
/** | ||
* @param {Options<Value>} options | ||
* Configuration. | ||
* @returns | ||
* Self. | ||
*/ | ||
constructor(options) { | ||
/** @type {Record<string, Array<Callback<Value>> | Value | Error | undefined>} */ | ||
this.cache = {} | ||
/** @type {string} */ | ||
this.cwd = options.cwd | ||
/** @type {boolean | undefined} */ | ||
this.detect = options.detect | ||
/** @type {Array<string>} */ | ||
this.names = options.names | ||
/** @type {Create<Value>} */ | ||
this.create = options.create | ||
if (fp) { | ||
self.givenFilePath = resolve(options.cwd, fp) | ||
/** @type {string | undefined} */ | ||
this.givenFilePath = options.filePath | ||
? path.resolve( | ||
options.cwd, | ||
typeof options.filePath === 'object' | ||
? fileURLToPath(options.filePath) | ||
: options.filePath | ||
) | ||
: undefined | ||
/** @type {Array<Callback<Value>> | Error | Value | undefined} */ | ||
this.givenFile | ||
} | ||
} | ||
function load(filePath, callback) { | ||
var self = this | ||
var cache = self.cache | ||
var givenFilePath = self.givenFilePath | ||
var givenFile = self.givenFile | ||
var names = self.names | ||
var create = self.create | ||
var cwd = self.cwd | ||
var parent | ||
/** | ||
* @param {string} filePath | ||
* File path to look from. | ||
* @param {Callback<Value>} callback | ||
* Callback called when done. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
load(filePath, callback) { | ||
const self = this | ||
const givenFile = this.givenFile | ||
const {givenFilePath} = this | ||
if (givenFilePath) { | ||
if (givenFile) { | ||
apply(callback, givenFile) | ||
} else { | ||
givenFile = [callback] | ||
self.givenFile = givenFile | ||
debug('Checking given file `%s`', givenFilePath) | ||
read(givenFilePath, loadGiven) | ||
} | ||
if (givenFilePath) { | ||
if (givenFile) { | ||
apply(callback, givenFile) | ||
} else { | ||
const self = this | ||
const cbs = [callback] | ||
this.givenFile = cbs | ||
debug('Checking given file `%s`', givenFilePath) | ||
fs.readFile(givenFilePath, function (cause, buf) { | ||
if (cause) { | ||
/** @type {NodeJS.ErrnoException} */ | ||
const result = new Error( | ||
'Cannot read given file `' + | ||
path.relative(self.cwd, givenFilePath) + | ||
'`', | ||
{cause} | ||
) | ||
// In `finder.js`, we check for `syscall`, to improve the error. | ||
result.code = 'ENOENT' | ||
result.path = cause.path | ||
result.syscall = cause.syscall | ||
loaded(result) | ||
} else { | ||
wrap(self.create, function (cause, /** @type {Value} */ result) { | ||
if (cause) { | ||
debug(cause.message) | ||
loaded( | ||
new Error( | ||
'Cannot parse given file `' + | ||
path.relative(self.cwd, givenFilePath) + | ||
'`', | ||
{cause} | ||
) | ||
) | ||
} else { | ||
debug('Read given file `%s`', givenFilePath) | ||
loaded(result) | ||
} | ||
})(buf, givenFilePath) | ||
} | ||
return | ||
} | ||
/** | ||
* @param {Error | Value} result | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function loaded(result) { | ||
self.givenFile = result | ||
applyAll(cbs, result) | ||
} | ||
}) | ||
} | ||
if (!self.detect) { | ||
return callback() | ||
} | ||
return | ||
} | ||
filePath = resolve(cwd, filePath) | ||
parent = dirname(filePath) | ||
if (!this.detect) { | ||
return callback(undefined) | ||
} | ||
if (parent in cache) { | ||
apply(callback, cache[parent]) | ||
} else { | ||
cache[parent] = [callback] | ||
find(parent) | ||
} | ||
filePath = path.resolve(this.cwd, filePath) | ||
const parent = path.dirname(filePath) | ||
function loadGiven(err, buf) { | ||
var cbs = self.givenFile | ||
var result | ||
if (err) { | ||
result = fault( | ||
'Cannot read given file `%s`\n%s', | ||
relative(cwd, givenFilePath), | ||
err.stack | ||
) | ||
result.code = 'ENOENT' | ||
result.path = err.path | ||
result.syscall = err.syscall | ||
if (parent in this.cache) { | ||
apply(callback, this.cache[parent]) | ||
} else { | ||
try { | ||
result = create(buf, givenFilePath) | ||
debug('Read given file `%s`', givenFilePath) | ||
} catch (err) { | ||
result = fault( | ||
'Cannot parse given file `%s`\n%s', | ||
relative(cwd, givenFilePath), | ||
err.stack | ||
) | ||
debug(err.message) | ||
} | ||
this.cache[parent] = [callback] | ||
find(parent) | ||
} | ||
givenFile = result | ||
self.givenFile = result | ||
applyAll(cbs, result) | ||
} | ||
/** | ||
* @param {string} folder | ||
* Folder. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function find(folder) { | ||
let index = -1 | ||
function find(directory) { | ||
var index = -1 | ||
var length = names.length | ||
next() | ||
next() | ||
/** | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function next() { | ||
// Try to read the next file. | ||
// We do not use `readdir` because on huge folders, that could be | ||
// *very* slow. | ||
if (++index < self.names.length) { | ||
fs.readFile(path.join(folder, self.names[index]), done) | ||
} else { | ||
const parent = path.dirname(folder) | ||
function next() { | ||
var parent | ||
/* Try to read the next file. We don’t use `readdir` because on | ||
* huge directories, that could be *very* slow. */ | ||
if (++index < length) { | ||
read(join(directory, names[index]), done) | ||
} else { | ||
parent = dirname(directory) | ||
if (directory === parent) { | ||
debug('No files found for `%s`', filePath) | ||
found() | ||
} else if (parent in cache) { | ||
apply(found, cache[parent]) | ||
} else { | ||
cache[parent] = [found] | ||
find(parent) | ||
if (folder === parent) { | ||
debug('No files found for `%s`', filePath) | ||
found(undefined) | ||
} else if (parent in self.cache) { | ||
apply(found, self.cache[parent]) | ||
} else { | ||
self.cache[parent] = [found] | ||
find(parent) | ||
} | ||
} | ||
} | ||
} | ||
function done(err, buf) { | ||
var name = names[index] | ||
var fp = join(directory, name) | ||
var contents | ||
/** | ||
* @param {NodeJS.ErrnoException | null} error | ||
* Error. | ||
* @param {Buffer | undefined} [buf] | ||
* File value. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function done(error, buf) { | ||
const fp = path.join(folder, self.names[index]) | ||
/* istanbul ignore if - Hard to test. */ | ||
if (err) { | ||
if (err.code === 'ENOENT') { | ||
return next() | ||
if (error) { | ||
if (error.code === 'ENOENT') { | ||
return next() | ||
/* c8 ignore next 11 -- hard to test other errors. */ | ||
} | ||
debug(error.message) | ||
return found( | ||
new Error( | ||
'Cannot read file `' + path.relative(self.cwd, fp) + '`', | ||
{cause: error} | ||
) | ||
) | ||
} | ||
err = fault('Cannot read file `%s`\n%s', relative(cwd, fp), err.message) | ||
debug(err.message) | ||
return found(err) | ||
wrap(self.create, function (cause, /** @type {Value} */ result) { | ||
if (cause) { | ||
found( | ||
new Error( | ||
'Cannot parse file `' + path.relative(self.cwd, fp) + '`', | ||
{cause} | ||
) | ||
) | ||
} else if (result && result.filePath) { | ||
debug('Read file `%s`', fp) | ||
found(undefined, result) | ||
} else { | ||
next() | ||
} | ||
})(buf, fp) | ||
} | ||
try { | ||
contents = create(buf, fp) | ||
} catch (err) { | ||
return found( | ||
fault('Cannot parse file `%s`\n%s', relative(cwd, fp), err.message) | ||
) | ||
/** | ||
* @type {Callback<Value>} | ||
* Callback called when done. | ||
*/ | ||
function found(error, result) { | ||
const cbs = self.cache[folder] | ||
assert(Array.isArray(cbs), 'always a list if found') | ||
self.cache[folder] = error || result | ||
applyAll(cbs, error || result) | ||
return undefined | ||
} | ||
/* istanbul ignore else - maybe used in the future. */ | ||
if (contents) { | ||
debug('Read file `%s`', fp) | ||
found(null, contents) | ||
} else { | ||
next() | ||
} | ||
} | ||
function found(err, result) { | ||
var cbs = cache[directory] | ||
cache[directory] = err || result | ||
applyAll(cbs, err || result) | ||
} | ||
} | ||
/** | ||
* @param {Array<Callback<Value>>} cbs | ||
* Callbacks. | ||
* @param {Error | Value | undefined} result | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function applyAll(cbs, result) { | ||
let index = cbs.length | ||
function applyAll(cbs, result) { | ||
var index = cbs.length | ||
while (index--) { | ||
apply(cbs[index], result) | ||
while (index--) { | ||
apply(cbs[index], result) | ||
} | ||
} | ||
} | ||
function apply(cb, result) { | ||
if (object(result) && typeof result[0] === 'function') { | ||
result.push(cb) | ||
} else if (result instanceof Error) { | ||
cb(result) | ||
} else { | ||
cb(null, result) | ||
/** | ||
* @param {Callback<Value>} cb | ||
* Callback. | ||
* @param {Array<Callback<Value>> | Error | Value | undefined} result | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function apply(cb, result) { | ||
if (Array.isArray(result)) { | ||
result.push(cb) | ||
} else if (result instanceof Error) { | ||
cb(result) | ||
} else { | ||
cb(undefined, result) | ||
} | ||
} | ||
} | ||
} |
@@ -1,66 +0,200 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('node:fs').Stats} Stats | ||
* @typedef {import('ignore').Ignore} IgnorePackageClass | ||
* @typedef {import('./ignore.js').Ignore} Ignore | ||
*/ | ||
var path = require('path') | ||
var fs = require('fs') | ||
var glob = require('glob') | ||
var vfile = require('to-vfile') | ||
var xtend = require('xtend') | ||
var hidden = require('is-hidden') | ||
var string = require('x-is-string') | ||
/** | ||
* @callback CheckCallback | ||
* Callback called when a file is checked. | ||
* @param {NodeJS.ErrnoException | undefined} error | ||
* Error. | ||
* @param {CheckResult | undefined} [result] | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
* | ||
* @typedef {CheckOptionsFields & Options} CheckOptions | ||
* Check options. | ||
* | ||
* @typedef CheckOptionsFields | ||
* Extra options for `check`. | ||
* @property {IgnorePackageClass} extraIgnore | ||
* Extra ignore. | ||
* | ||
* @typedef CheckResult | ||
* Result. | ||
* @property {Stats | undefined} stats | ||
* Stats. | ||
* @property {boolean | undefined} ignored | ||
* Whether the file is ignored. | ||
* | ||
* @callback ExpandCallback | ||
* Callback called when files are expanded. | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {ExpandResult | undefined} [result] | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
* | ||
* @typedef ExpandResult | ||
* Results. | ||
* @property {Array<VFile | string>} input | ||
* Input. | ||
* @property {Array<VFile>} output | ||
* Output. | ||
* | ||
* @callback FindCallback | ||
* Callback called when files are found. | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {FindResult | undefined} [result] | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
* | ||
* @typedef FindResult | ||
* Results. | ||
* @property {boolean} oneFileMode | ||
* Whether we looked for an explicit single file only. | ||
* @property {Array<VFile>} files | ||
* Results. | ||
* | ||
* @typedef Options | ||
* Configuration. | ||
* @property {string} cwd | ||
* Base. | ||
* @property {Array<string>} extensions | ||
* Extnames. | ||
* @property {boolean | undefined} silentlyIgnore | ||
* Whether to silently ignore errors. | ||
* | ||
* The default is to throw if an explicitly given file is explicitly ignored. | ||
* @property {Array<string>} ignorePatterns | ||
* Extra ignore patterns. | ||
* @property {Ignore} ignore | ||
* Ignore. | ||
* | ||
* @callback SearchCallback | ||
* Callback called after searching. | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {Array<VFile> | undefined} [result] | ||
* Result. | ||
* @returns {undefined} | ||
* Nothing. | ||
* | ||
* @typedef {Options & SearchOptionsFields} SearchOptions | ||
* Search options. | ||
* | ||
* @typedef SearchOptionsFields | ||
* Extra search fields. | ||
* @property {boolean | undefined} [nested] | ||
* Whether this is a nested search. | ||
*/ | ||
var readdir = fs.readdir | ||
var stat = fs.stat | ||
var join = path.join | ||
var relative = path.relative | ||
var resolve = path.resolve | ||
var basename = path.basename | ||
var extname = path.extname | ||
var magic = glob.hasMagic | ||
import path from 'node:path' | ||
import fs from 'node:fs' | ||
import {glob, hasMagic} from 'glob' | ||
import ignore_ from 'ignore' | ||
import {VFile} from 'vfile' | ||
module.exports = find | ||
// @ts-expect-error: types of `ignore` are wrong. | ||
const ignore = /** @type {import('ignore')['default']} */ (ignore_) | ||
/* Search `patterns`, a mix of globs, paths, and files. */ | ||
function find(input, options, callback) { | ||
expand(input, options, done) | ||
function done(err, result) { | ||
/* istanbul ignore if - glob errors are unusual. | ||
* other errors are on the vfile results. */ | ||
if (err) { | ||
callback(err) | ||
/** | ||
* Search `input`, a mix of globs, paths, and files. | ||
* | ||
* @param {Array<VFile | string>} input | ||
* Files, file paths, and globs. | ||
* @param {Options} options | ||
* Configuration (required). | ||
* @param {FindCallback} callback | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function finder(input, options, callback) { | ||
expand(input, options, function (error, result) { | ||
/* c8 ignore next 2 -- glob errors are unusual. */ | ||
if (error || !result) { | ||
callback(error) | ||
} else { | ||
callback(null, {oneFileMode: oneFileMode(result), files: result.output}) | ||
callback(undefined, { | ||
files: result.output, | ||
oneFileMode: oneFileMode(result) | ||
}) | ||
} | ||
} | ||
}) | ||
} | ||
/* Expand the given glob patterns, search given and found | ||
* directories, and map to vfiles. */ | ||
/** | ||
* Expand the given glob patterns, search given and found folders, and map | ||
* to vfiles. | ||
* | ||
* @param {Array<VFile | string>} input | ||
* List of files, file paths, and globs. | ||
* @param {Options} options | ||
* Configuration (required). | ||
* @param {ExpandCallback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function expand(input, options, next) { | ||
var cwd = options.cwd | ||
var paths = [] | ||
var actual = 0 | ||
var expected = 0 | ||
var failed | ||
/** @type {Array<VFile | string>} */ | ||
const paths = [] | ||
let actual = 0 | ||
let expected = 0 | ||
let index = -1 | ||
/** @type {boolean | undefined} */ | ||
let failed | ||
input.forEach(each) | ||
while (++index < input.length) { | ||
let file = input[index] | ||
if (typeof file === 'string') { | ||
if (hasMagic(file)) { | ||
expected++ | ||
glob(file, {cwd: options.cwd}).then( | ||
function (files) { | ||
/* c8 ignore next 3 -- glob errors are unusual. */ | ||
if (failed) { | ||
return | ||
} | ||
if (!expected) { | ||
search(paths, options, done) | ||
} | ||
actual++ | ||
paths.push(...files) | ||
function each(file) { | ||
if (string(file)) { | ||
if (magic(file)) { | ||
expected++ | ||
glob(file, {cwd: cwd}, one) | ||
if (actual === expected) { | ||
search(paths, options, done1) | ||
} | ||
}, | ||
/** | ||
* @param {Error} error | ||
* Error. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
/* c8 ignore next 8 -- glob errors are unusual. */ | ||
function (error) { | ||
if (failed) { | ||
return | ||
} | ||
failed = true | ||
done1(error) | ||
} | ||
) | ||
} else { | ||
/* `relative` to make the paths canonical. */ | ||
file = relative(cwd, resolve(cwd, file)) || '.' | ||
// `relative` to make the paths canonical. | ||
file = | ||
path.relative(options.cwd, path.resolve(options.cwd, file)) || '.' | ||
paths.push(file) | ||
} | ||
} else { | ||
file.cwd = cwd | ||
file.path = relative(cwd, file.path) | ||
file.history = [file.path] | ||
const fp = file.path ? path.relative(options.cwd, file.path) : options.cwd | ||
file.cwd = options.cwd | ||
file.path = fp | ||
file.history = [fp] | ||
paths.push(file) | ||
@@ -70,28 +204,20 @@ } | ||
function one(err, files) { | ||
/* istanbul ignore if - glob errors are unusual. */ | ||
if (failed) { | ||
return | ||
} | ||
/* istanbul ignore if - glob errors are unusual. */ | ||
if (err) { | ||
failed = true | ||
done(err) | ||
} else { | ||
actual++ | ||
paths = paths.concat(files) | ||
if (actual === expected) { | ||
search(paths, options, done) | ||
} | ||
} | ||
if (!expected) { | ||
search(paths, options, done1) | ||
} | ||
function done(err, files) { | ||
/* istanbul ignore if - `search` currently does not give errors. */ | ||
if (err) { | ||
next(err) | ||
/** | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {Array<VFile> | undefined} [files] | ||
* List of files. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function done1(error, files) { | ||
/* c8 ignore next 2 -- `search` currently does not give errors. */ | ||
if (error || !files) { | ||
next(error) | ||
} else { | ||
next(null, {input: paths, output: files}) | ||
next(undefined, {input: paths, output: files}) | ||
} | ||
@@ -101,24 +227,47 @@ } | ||
/* Search `paths`. */ | ||
/** | ||
* Search `paths`. | ||
* | ||
* @param {Array<VFile | string>} input | ||
* List of files, file paths, and globs. | ||
* @param {SearchOptions} options | ||
* Configuration (required). | ||
* @param {SearchCallback} next | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function search(input, options, next) { | ||
var cwd = options.cwd | ||
var silent = options.silentlyIgnore | ||
var nested = options.nested | ||
var extensions = options.extensions | ||
var files = [] | ||
var expected = 0 | ||
var actual = 0 | ||
const extraIgnore = ignore().add(options.ignorePatterns) | ||
let expected = 0 | ||
let actual = 0 | ||
let index = -1 | ||
/** @type {Array<VFile>} */ | ||
const files = [] | ||
input.forEach(each) | ||
while (++index < input.length) { | ||
each(input[index]) | ||
} | ||
if (!expected) { | ||
next(null, files) | ||
next(undefined, files) | ||
} | ||
return each | ||
/** | ||
* @param {VFile | string} file | ||
* File or file path. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function each(file) { | ||
var part = base(file) | ||
const ext = typeof file === 'string' ? path.extname(file) : file.extname | ||
if (nested && (hidden(part) || part === 'node_modules')) { | ||
// Normalise globs. | ||
if (typeof file === 'string') { | ||
file = file.split('/').join(path.sep) | ||
} | ||
const part = base(file) | ||
if (options.nested && part && part === 'node_modules') { | ||
return | ||
@@ -129,67 +278,96 @@ } | ||
statAndIgnore(file, options, handle) | ||
check( | ||
file, | ||
{...options, extraIgnore}, | ||
/** | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function (error, result) { | ||
const ignored = result && result.ignored | ||
const dir = result && result.stats && result.stats.isDirectory() | ||
function handle(err, result) { | ||
var ignored = result && result.ignored | ||
var dir = result && result.stats && result.stats.isDirectory() | ||
if (ignored && (options.nested || options.silentlyIgnore)) { | ||
return one(undefined, []) | ||
} | ||
if (ignored && (nested || silent)) { | ||
return one(null, []) | ||
} | ||
if (!ignored && dir) { | ||
fs.readdir( | ||
path.resolve(options.cwd, filePath(file)), | ||
function (error, basenames) { | ||
/* c8 ignore next 11 -- should not happen: the folder is `stat`ed ok, but reading it is not. */ | ||
if (error) { | ||
const otherFile = new VFile({path: filePath(file)}) | ||
otherFile.cwd = options.cwd | ||
if (!ignored && dir) { | ||
return readdir(resolve(cwd, filePath(file)), directory) | ||
} | ||
try { | ||
otherFile.fail('Cannot read folder') | ||
} catch { | ||
// Empty. | ||
} | ||
if ( | ||
nested && | ||
!dir && | ||
extensions.length !== 0 && | ||
extensions.indexOf(extname(file)) === -1 | ||
) { | ||
return one(null, []) | ||
} | ||
one(undefined, [otherFile]) | ||
} else { | ||
search( | ||
basenames.map(function (name) { | ||
return path.join(filePath(file), name) | ||
}), | ||
{...options, nested: true}, | ||
one | ||
) | ||
} | ||
} | ||
) | ||
return | ||
} | ||
file = vfile(file) | ||
file.cwd = cwd | ||
if ( | ||
!dir && | ||
options.nested && | ||
options.extensions.length > 0 && | ||
(!ext || !options.extensions.includes(ext)) | ||
) { | ||
return one(undefined, []) | ||
} | ||
if (ignored) { | ||
try { | ||
file.fail('Cannot process specified file: it’s ignored') | ||
} catch (err) {} | ||
} | ||
file = typeof file === 'string' ? new VFile({path: file}) : file | ||
file.cwd = options.cwd | ||
if (err && err.code === 'ENOENT') { | ||
try { | ||
file.fail(err.syscall === 'stat' ? 'No such file or directory' : err) | ||
} catch (err) {} | ||
} | ||
if (ignored) { | ||
const message = file.message( | ||
'Cannot process specified file: it’s ignored' | ||
) | ||
message.fatal = true | ||
} | ||
one(null, [file]) | ||
} | ||
if (error && error.code === 'ENOENT') { | ||
if (error.syscall === 'stat') { | ||
const message = file.message('No such file or folder', { | ||
cause: error | ||
}) | ||
message.fatal = true | ||
} else { | ||
const message = file.message('Cannot find file', {cause: error}) | ||
message.fatal = true | ||
} | ||
} | ||
function directory(err, basenames) { | ||
var file | ||
/* istanbul ignore if - Should not happen often: the directory | ||
* is `stat`ed first, which was ok, but reading it is not. */ | ||
if (err) { | ||
file = vfile(filePath(file)) | ||
file.cwd = cwd | ||
try { | ||
file.fail('Cannot read directory') | ||
} catch (err) {} | ||
one(null, [file]) | ||
} else { | ||
search(basenames.map(concat), xtend(options, {nested: true}), one) | ||
one(undefined, [file]) | ||
} | ||
} | ||
) | ||
/* Error is never given. Always given `results`. */ | ||
/** | ||
* Error is never given. Always given `results`. | ||
* | ||
* @param {Error | undefined} _ | ||
* Error. | ||
* @param {Array<VFile> | undefined} [results] | ||
* Results. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function one(_, results) { | ||
/* istanbul ignore else - always given. */ | ||
/* istanbul ignore else - Always given. */ | ||
if (results) { | ||
files = files.concat(results) | ||
files.push(...results) | ||
} | ||
@@ -200,45 +378,71 @@ | ||
if (actual === expected) { | ||
next(null, files) | ||
next(undefined, files) | ||
} | ||
} | ||
function concat(value) { | ||
return join(filePath(file), value) | ||
} | ||
} | ||
} | ||
function statAndIgnore(file, options, callback) { | ||
var ignore = options.ignore | ||
var fp = resolve(options.cwd, filePath(file)) | ||
var expected = 1 | ||
var actual = 0 | ||
var stats | ||
var ignored | ||
/** | ||
* @param {VFile | string} file | ||
* File. | ||
* @param {CheckOptions} options | ||
* Configuration. | ||
* @param {CheckCallback} callback | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function check(file, options, callback) { | ||
const fp = path.resolve(options.cwd, filePath(file)) | ||
const normal = path.relative(options.cwd, fp) | ||
let expected = 1 | ||
let actual = 0 | ||
/** @type {Stats | undefined} */ | ||
let stats | ||
/** @type {boolean | undefined} */ | ||
let ignored | ||
if (!file.contents) { | ||
if ( | ||
typeof file === 'string' || | ||
file.value === null || | ||
file.value === undefined | ||
) { | ||
expected++ | ||
stat(fp, handleStat) | ||
fs.stat(fp, function (error, value) { | ||
stats = value | ||
onStatOrCheck(error || undefined) | ||
}) | ||
} | ||
ignore.check(fp, handleIgnore) | ||
options.ignore.check(fp, function (error, value) { | ||
ignored = value | ||
function handleStat(err, value) { | ||
stats = value | ||
one(err) | ||
} | ||
// `ignore.check` is sometimes sync, we need to force async behavior. | ||
setImmediate(onStatOrCheck, error || undefined) | ||
}) | ||
function handleIgnore(err, value) { | ||
ignored = value | ||
one(err) | ||
} | ||
function one(err) { | ||
/** | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function onStatOrCheck(error) { | ||
actual++ | ||
if (err) { | ||
callback(err) | ||
if (error) { | ||
callback(error) | ||
actual = -1 | ||
} else if (actual === expected) { | ||
callback(null, {stats: stats, ignored: ignored}) | ||
callback(undefined, { | ||
ignored: | ||
ignored || | ||
(normal === '' || | ||
normal === '..' || | ||
normal.charAt(0) === path.sep || | ||
normal.slice(0, 3) === '..' + path.sep | ||
? false | ||
: options.extraIgnore.ignores(normal)), | ||
stats | ||
}) | ||
} | ||
@@ -248,10 +452,28 @@ } | ||
/** | ||
* @param {VFile | string} file | ||
* File. | ||
* @returns {string | undefined} | ||
* Basename. | ||
*/ | ||
function base(file) { | ||
return string(file) ? basename(file) : file.basename | ||
return typeof file === 'string' ? path.basename(file) : file.basename | ||
} | ||
/** | ||
* @param {VFile | string} file | ||
* File. | ||
* @returns {string} | ||
* File path. | ||
*/ | ||
function filePath(file) { | ||
return string(file) ? file : file.path | ||
return typeof file === 'string' ? file : file.path | ||
} | ||
/** | ||
* @param {ExpandResult} result | ||
* Result. | ||
* @returns {boolean} | ||
* Whether we looked for an explicit single file only. | ||
*/ | ||
function oneFileMode(result) { | ||
@@ -258,0 +480,0 @@ return ( |
@@ -1,50 +0,124 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('ignore').Ignore} IgnorePackageClass | ||
*/ | ||
var path = require('path') | ||
var gitignore = require('ignore') | ||
var FindUp = require('./find-up') | ||
/** | ||
* @callback Callback | ||
* Callback. | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {boolean | undefined} [result] | ||
* Whether to ignore. | ||
* @returns {undefined} | ||
* Nothing. | ||
* | ||
* @typedef Options | ||
* Configuration. | ||
* @property {string} cwd | ||
* Base. | ||
* @property {boolean | undefined} detectIgnore | ||
* Whether to detect ignore files. | ||
* @property {string | undefined} ignoreName | ||
* Basename of ignore files. | ||
* @property {URL | string | undefined} ignorePath | ||
* Explicit path to an ignore file. | ||
* @property {ResolveFrom | undefined} ignorePathResolveFrom | ||
* How to resolve. | ||
* | ||
* @typedef {'cwd' | 'dir'} ResolveFrom | ||
* How to resolve. | ||
* | ||
* @typedef {IgnorePackageClass & ResultFields} Result | ||
* Result. | ||
* | ||
* @typedef ResultFields | ||
* Extra fields. | ||
* @property {string} filePath | ||
* File path. | ||
* | ||
*/ | ||
module.exports = Ignore | ||
import path from 'node:path' | ||
import ignore_ from 'ignore' | ||
import {FindUp} from './find-up.js' | ||
Ignore.prototype.check = check | ||
// @ts-expect-error: types of `ignore` are wrong. | ||
const ignore = /** @type {import('ignore')['default']} */ (ignore_) | ||
var dirname = path.dirname | ||
var relative = path.relative | ||
var resolve = path.resolve | ||
export class Ignore { | ||
/** | ||
* @param {Options} options | ||
* Configuration. | ||
* @returns | ||
* Self. | ||
*/ | ||
constructor(options) { | ||
/** @type {string} */ | ||
this.cwd = options.cwd | ||
/** @type {ResolveFrom | undefined} */ | ||
this.ignorePathResolveFrom = options.ignorePathResolveFrom | ||
function Ignore(options) { | ||
this.cwd = options.cwd | ||
/** @type {FindUp<Result>} */ | ||
this.findUp = new FindUp({ | ||
create, | ||
cwd: options.cwd, | ||
detect: options.detectIgnore, | ||
filePath: options.ignorePath, | ||
names: options.ignoreName ? [options.ignoreName] : [] | ||
}) | ||
} | ||
this.findUp = new FindUp({ | ||
filePath: options.ignorePath, | ||
cwd: options.cwd, | ||
detect: options.detectIgnore, | ||
names: options.ignoreName ? [options.ignoreName] : [], | ||
create: create | ||
}) | ||
} | ||
/** | ||
* @param {string} filePath | ||
* File path. | ||
* @param {Callback} callback | ||
* Callback | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
check(filePath, callback) { | ||
const self = this | ||
function check(filePath, callback) { | ||
var self = this | ||
this.findUp.load(filePath, function (error, ignoreSet) { | ||
if (error) { | ||
callback(error) | ||
} else if (ignoreSet) { | ||
const normal = path.relative( | ||
path.resolve( | ||
self.cwd, | ||
self.ignorePathResolveFrom === 'cwd' ? '.' : ignoreSet.filePath | ||
), | ||
path.resolve(self.cwd, filePath) | ||
) | ||
self.findUp.load(filePath, done) | ||
function done(err, ignore) { | ||
var normal | ||
if (err) { | ||
callback(err) | ||
} else if (ignore) { | ||
normal = relative(ignore.filePath, resolve(self.cwd, filePath)) | ||
callback(null, normal ? ignore.ignores(normal) : false) | ||
} else { | ||
callback(null, false) | ||
} | ||
if ( | ||
normal === '' || | ||
normal === '..' || | ||
normal.charAt(0) === path.sep || | ||
normal.slice(0, 3) === '..' + path.sep | ||
) { | ||
callback(undefined, false) | ||
} else { | ||
callback(undefined, ignoreSet.ignores(normal)) | ||
} | ||
} else { | ||
callback(undefined, false) | ||
} | ||
}) | ||
} | ||
} | ||
/** | ||
* @param {Buffer} buf | ||
* File value. | ||
* @param {string} filePath | ||
* File path. | ||
* @returns {Result} | ||
* Result. | ||
*/ | ||
function create(buf, filePath) { | ||
var ignore = gitignore().add(String(buf)) | ||
ignore.filePath = dirname(filePath) | ||
return ignore | ||
// Cast so we can patch `filePath`. | ||
const result = /** @type {Result} */ (ignore().add(String(buf))) | ||
result.filePath = path.dirname(filePath) | ||
return result | ||
} |
386
lib/index.js
@@ -1,29 +0,231 @@ | ||
'use strict' | ||
/** | ||
* @typedef {import('unified').Processor<any, any, any, any, any>} Processor | ||
* @typedef {import('unified').Settings} UnifiedSettings | ||
* | ||
* @typedef {import('vfile').VFile} VFile | ||
* | ||
* @typedef {import('vfile-reporter').Options} VFileReporterKnownFields | ||
* | ||
* @typedef {import('./configuration.js').ConfigTransform} ConfigTransform | ||
* @typedef {import('./configuration.js').PresetSupportingSpecifiers} PresetSupportingSpecifiers | ||
* @typedef {import('./file-set.js').FileSet} FileSet | ||
* @typedef {import('./ignore.js').ResolveFrom} ResolveFrom | ||
*/ | ||
var PassThrough = require('stream').PassThrough | ||
var statistics = require('vfile-statistics') | ||
var fileSetPipeline = require('./file-set-pipeline') | ||
/** | ||
* @callback Callback | ||
* Callback called when done. | ||
* | ||
* Called with a fatal error if things went horribly wrong (probably due to | ||
* incorrect configuration), or a status code and the processing context. | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {0 | 1 | undefined} [code] | ||
* Exit code, `0` if successful or `1` if unsuccessful. | ||
* @param {Context | undefined} [context] | ||
* Processing context. | ||
* @returns {undefined | void} | ||
* Nothing. | ||
* | ||
* Note: `void` included because `promisify` otherwise fails. | ||
* | ||
* @typedef Context | ||
* Processing context. | ||
* @property {FileSet | undefined} fileSet | ||
* Internally used info. | ||
* @property {Array<VFile> | undefined} files | ||
* Processed files. | ||
* | ||
* @typedef Options | ||
* Configuration. | ||
* | ||
* > 👉 **Note**: `options.processor` is required. | ||
* @property {boolean | undefined} [alwaysStringify=false] | ||
* Whether to always serialize successfully processed files (default: | ||
* `false`). | ||
* @property {boolean | undefined} [color=false] | ||
* Whether to report with ANSI color sequences (default: `false`); given to | ||
* the reporter. | ||
* @property {ConfigTransform | undefined} [configTransform] | ||
* Transform config files from a different schema (optional). | ||
* @property {URL | string | undefined} [cwd] | ||
* Folder to search files in, load plugins from, and more (default: | ||
* `process.cwd()`). | ||
* @property {PresetSupportingSpecifiers | undefined} [defaultConfig] | ||
* Default configuration to use if no config file is given or found | ||
* (optional). | ||
* @property {boolean | undefined} [detectConfig] | ||
* Whether to search for configuration files (default: `true` if | ||
* `options.packageField` or `options.rcName`). | ||
* @property {boolean | undefined} [detectIgnore] | ||
* Whether to search for ignore files (default: `true` if | ||
* `options.ignoreName`). | ||
* @property {Array<string> | undefined} [extensions] | ||
* Search for files with these extensions, when folders are passed | ||
* (optional); generated files are also given the first extension if `treeIn` | ||
* is on and `output` is on or points to a folder. | ||
* @property {URL | string | undefined} [filePath] | ||
* File path to process the given file on `streamIn` as (optional). | ||
* @property {Array<URL | VFile | string> | undefined} [files] | ||
* Paths or globs to files and folders, or virtual files, to process | ||
* (optional). | ||
* @property {boolean | undefined} [frail=false] | ||
* Call back with an unsuccessful (`1`) code on warnings as well as errors | ||
* (default: `false`). | ||
* @property {string | undefined} [ignoreName] | ||
* Name of ignore files to load (optional). | ||
* @property {URL | string | undefined} [ignorePath] | ||
* Filepath to an ignore file to load (optional). | ||
* @property {ResolveFrom | undefined} [ignorePathResolveFrom] | ||
* Resolve patterns in `ignorePath` from the current working | ||
* directory (`'cwd'`) or the ignore file’s folder (`'dir'`) (default: | ||
* `'dir'`). | ||
* @property {Array<string> | undefined} [ignorePatterns] | ||
* Patterns to ignore in addition to ignore files (optional). | ||
* @property {boolean | undefined} [ignoreUnconfigured=false] | ||
* Ignore files that do not have an associated detected configuration file | ||
* (default: `false`); either `rcName` or `packageField` must be defined too; | ||
* cannot be combined with `rcPath` or `detectConfig: false`. | ||
* @property {boolean | undefined} [inspect=false] | ||
* Whether to output a formatted syntax tree for debugging (default: | ||
* `false`). | ||
* @property {boolean | undefined} [out=false] | ||
* Whether to write the processed file to `streamOut` (default: `false`). | ||
* @property {URL | boolean | string | undefined} [output=false] | ||
* Whether to write successfully processed files, and where to (default: | ||
* `false`). | ||
* | ||
* * When `true`, overwrites the given files | ||
* * When `false`, does not write to the file system | ||
* * When pointing to an existing folder, files are written to that | ||
* folder and keep their original basenames | ||
* * When the parent folder of the given path exists and one file is | ||
* processed, the file is written to the given path | ||
* @property {string | undefined} [packageField] | ||
* Field where configuration can be found in `package.json` files | ||
* (optional). | ||
* @property {string | undefined} [pluginPrefix] | ||
* Prefix to use when searching for plugins (optional). | ||
* @property {PresetSupportingSpecifiers['plugins'] | undefined} [plugins] | ||
* Plugins to use (optional). | ||
* @property {() => Processor} processor | ||
* Unified processor to transform files (required). | ||
* @property {boolean | undefined} [quiet=false] | ||
* Do not report successful files (default: `false`); given to the reporter. | ||
* @property {string | undefined} [rcName] | ||
* Name of configuration files to load (optional). | ||
* @property {string | undefined} [rcPath] | ||
* Filepath to a configuration file to load (optional). | ||
* @property {VFileReporter | string | undefined} [reporter] | ||
* Reporter to use (default: `'vfile-reporter'`); if a `string` is passed, | ||
* it’s loaded from `cwd`, and `'vfile-reporter-'` can be omitted | ||
* @property {VFileReporterOptions | undefined} [reporterOptions] | ||
* Config to pass to the used reporter (optional). | ||
* @property {UnifiedSettings | undefined} [settings] | ||
* Configuration for the parser and compiler of the processor (optional). | ||
* @property {boolean | undefined} [silent=false] | ||
* Report only fatal errors (default: `false`); given to the reporter. | ||
* @property {boolean | undefined} [silentlyIgnore=false] | ||
* Skip given files if they are ignored (default: `false`). | ||
* @property {NodeJS.WritableStream | undefined} [streamError] | ||
* Stream to write the report (if any) to (default: `process.stderr`). | ||
* @property {NodeJS.ReadableStream | undefined} [streamIn] | ||
* Stream to read from if no files are given (default: `process.stdin`). | ||
* @property {NodeJS.WritableStream | undefined} [streamOut] | ||
* Stream to write processed files to (default: `process.stdout`); nothing is | ||
* streamed if either `out` is `false`, `output` is not `false`, multiple | ||
* files are processed, or a fatal error occurred while processing a file. | ||
* @property {boolean | undefined} [tree=false] | ||
* Whether to treat both input and output as a syntax tree (default: | ||
* `false`). | ||
* @property {boolean | undefined} [treeIn] | ||
* Whether to treat input as a syntax tree (default: `options.tree`). | ||
* @property {boolean | undefined} [treeOut] | ||
* Whether to output as a syntax tree (default: `options.tree`). | ||
* | ||
* @typedef Settings | ||
* Resolved {@link Options `Options`} passed around. | ||
* @property {Options['processor']} processor | ||
* @property {Exclude<Options['cwd'], URL | undefined>} cwd | ||
* @property {Array<VFile | string>} files | ||
* @property {Exclude<Options['extensions'], undefined>} extensions | ||
* @property {Exclude<Options['streamIn'], undefined>} streamIn | ||
* @property {Options['filePath']} filePath | ||
* @property {Exclude<Options['streamOut'], undefined>} streamOut | ||
* @property {Exclude<Options['streamError'], undefined>} streamError | ||
* @property {Options['out']} out | ||
* @property {Options['output']} output | ||
* @property {Options['alwaysStringify']} alwaysStringify | ||
* @property {Options['tree']} tree | ||
* @property {Options['treeIn']} treeIn | ||
* @property {Options['treeOut']} treeOut | ||
* @property {Options['inspect']} inspect | ||
* @property {Options['rcName']} rcName | ||
* @property {Options['packageField']} packageField | ||
* @property {Options['detectConfig']} detectConfig | ||
* @property {Options['rcPath']} rcPath | ||
* @property {Exclude<Options['settings'], undefined>} settings | ||
* @property {Options['ignoreName']} ignoreName | ||
* @property {Options['detectIgnore']} detectIgnore | ||
* @property {Options['ignorePath']} ignorePath | ||
* @property {Options['ignorePathResolveFrom']} ignorePathResolveFrom | ||
* @property {Exclude<Options['ignorePatterns'], undefined>} ignorePatterns | ||
* @property {Exclude<Options['ignoreUnconfigured'], undefined>} ignoreUnconfigured | ||
* @property {Exclude<Options['silentlyIgnore'], undefined>} silentlyIgnore | ||
* @property {Options['plugins']} plugins | ||
* @property {Options['pluginPrefix']} pluginPrefix | ||
* @property {Options['configTransform']} configTransform | ||
* @property {Options['defaultConfig']} defaultConfig | ||
* @property {Options['reporter']} reporter | ||
* @property {Options['reporterOptions']} reporterOptions | ||
* @property {Options['color']} color | ||
* @property {Options['silent']} silent | ||
* @property {Options['quiet']} quiet | ||
* @property {Options['frail']} frail | ||
* | ||
* @callback VFileReporter | ||
* Reporter. | ||
* | ||
* This is essentially the interface of `vfile-reporter`, with added support | ||
* for unknown fields in options and async support. | ||
* @param {Array<VFile>} files | ||
* Files. | ||
* @param {VFileReporterOptions} options | ||
* Configuration. | ||
* @returns {Promise<string> | string} | ||
* Report. | ||
* | ||
* @typedef {VFileReporterKnownFields & {[key: string]: unknown}} VFileReporterOptions | ||
* Configuration. | ||
*/ | ||
module.exports = run | ||
import process from 'node:process' | ||
import {PassThrough} from 'node:stream' | ||
import {fileURLToPath} from 'node:url' | ||
import {statistics} from 'vfile-statistics' | ||
import {fileSetPipeline} from './file-set-pipeline/index.js' | ||
/* Run the file set pipeline once. | ||
* `callback` is invoked with a fatal error, | ||
* or with a status code (`0` on success, `1` on failure). */ | ||
function run(options, callback) { | ||
var settings = {} | ||
var stdin = new PassThrough() | ||
var tree | ||
var detectConfig | ||
var hasConfig | ||
var detectIgnore | ||
var hasIgnore | ||
/** | ||
* Process. | ||
* | ||
* @param {Options} options | ||
* Configuration (required). | ||
* @param {Callback} callback | ||
* Callback. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
export function engine(options, callback) { | ||
/** @type {Settings} */ | ||
const settings = {} | ||
/** @type {NodeJS.ReadStream | PassThrough} */ | ||
let stdin = new PassThrough() | ||
try { | ||
stdin = process.stdin | ||
} catch (err) { | ||
/* Obscure bug in Node (seen on windows): | ||
* - https://github.com/nodejs/node/blob/f856234/lib/internal/ | ||
* process/stdio.js#L82; | ||
* - https://github.com/AtomLinter/linter-markdown/pull/85. | ||
*/ | ||
// See: <https://github.com/nodejs/node/blob/f856234/lib/internal/process/stdio.js#L82>, | ||
// <https://github.com/AtomLinter/linter-markdown/pull/85>. | ||
/* c8 ignore next 3 -- obscure bug in Node (seen on Windows). */ | ||
} catch { | ||
// Empty. | ||
} | ||
@@ -39,16 +241,24 @@ | ||
/* Processor. */ | ||
// Processor. | ||
settings.processor = options.processor | ||
/* Path to run as. */ | ||
settings.cwd = options.cwd || process.cwd() | ||
// Path to run as. | ||
settings.cwd = | ||
typeof options.cwd === 'object' | ||
? fileURLToPath(options.cwd) | ||
: options.cwd || process.cwd() | ||
/* Input. */ | ||
settings.files = options.files || [] | ||
settings.extensions = (options.extensions || []).map(extension) | ||
// Input. | ||
settings.files = (options.files || []).map(function (d) { | ||
const result = isUrl(d) ? fileURLToPath(d) : d | ||
return result | ||
}) | ||
settings.extensions = (options.extensions || []).map(function (ext) { | ||
return ext.charAt(0) === '.' ? ext : '.' + ext | ||
}) | ||
settings.filePath = options.filePath || null | ||
settings.filePath = options.filePath | ||
settings.streamIn = options.streamIn || stdin | ||
/* Output. */ | ||
// Output. | ||
settings.streamOut = options.streamOut || process.stdout | ||
@@ -60,3 +270,3 @@ settings.streamError = options.streamError || process.stderr | ||
/* Null overwrites config settings, `undefined` doesn’t. */ | ||
// Null overwrites config settings, `undefined` does not. | ||
if (settings.output === null || settings.output === undefined) { | ||
@@ -70,4 +280,4 @@ settings.output = undefined | ||
/* Process phase management. */ | ||
tree = options.tree || false | ||
// Process phase management. | ||
const tree = options.tree || false | ||
@@ -86,5 +296,5 @@ settings.treeIn = options.treeIn | ||
/* Configuration. */ | ||
detectConfig = options.detectConfig | ||
hasConfig = Boolean(options.rcName || options.packageField) | ||
// Configuration. | ||
const detectConfig = options.detectConfig | ||
const hasConfig = Boolean(options.rcName || options.packageField) | ||
@@ -101,5 +311,5 @@ if (detectConfig && !hasConfig) { | ||
: detectConfig | ||
settings.rcName = options.rcName || null | ||
settings.rcPath = options.rcPath || null | ||
settings.packageField = options.packageField || null | ||
settings.rcName = options.rcName | ||
settings.rcPath = options.rcPath | ||
settings.packageField = options.packageField | ||
settings.settings = options.settings || {} | ||
@@ -109,5 +319,6 @@ settings.configTransform = options.configTransform | ||
/* Ignore. */ | ||
detectIgnore = options.detectIgnore | ||
hasIgnore = Boolean(options.ignoreName) | ||
// Ignore. | ||
const detectIgnore = options.detectIgnore | ||
const hasIgnore = Boolean(options.ignoreName) | ||
const ignoreUnconfigured = Boolean(options.ignoreUnconfigured) | ||
@@ -118,6 +329,33 @@ settings.detectIgnore = | ||
: detectIgnore | ||
settings.ignoreName = options.ignoreName || null | ||
settings.ignorePath = options.ignorePath || null | ||
settings.ignoreName = options.ignoreName | ||
settings.ignorePath = options.ignorePath | ||
settings.ignorePathResolveFrom = options.ignorePathResolveFrom || 'dir' | ||
settings.ignorePatterns = options.ignorePatterns || [] | ||
settings.ignoreUnconfigured = ignoreUnconfigured | ||
settings.silentlyIgnore = Boolean(options.silentlyIgnore) | ||
if (ignoreUnconfigured && settings.rcPath) { | ||
return next( | ||
new Error( | ||
'Cannot accept both `rcPath` and `ignoreUnconfigured`, as former prevents looking for configuration but the latter requires it' | ||
) | ||
) | ||
} | ||
if (ignoreUnconfigured && !hasConfig) { | ||
return next( | ||
new Error( | ||
'Missing `rcName` or `packageField` with `ignoreUnconfigured`, the former are needed to look for configuration' | ||
) | ||
) | ||
} | ||
if (ignoreUnconfigured && !settings.detectConfig) { | ||
return next( | ||
new Error( | ||
'Cannot use `detectConfig: false` with `ignoreUnconfigured`, the former prevents looking for configuration but the latter requires it' | ||
) | ||
) | ||
} | ||
if (detectIgnore && !hasIgnore) { | ||
@@ -127,27 +365,35 @@ return next(new Error('Missing `ignoreName` with `detectIgnore`')) | ||
/* Plug-ins. */ | ||
settings.pluginPrefix = options.pluginPrefix || null | ||
settings.plugins = options.plugins || {} | ||
// Plugins. | ||
settings.pluginPrefix = options.pluginPrefix | ||
settings.plugins = options.plugins || [] | ||
/* Reporting. */ | ||
settings.reporter = options.reporter || null | ||
settings.reporterOptions = options.reporterOptions || null | ||
// Reporting. | ||
settings.reporter = options.reporter | ||
settings.reporterOptions = options.reporterOptions | ||
settings.color = options.color || false | ||
settings.silent = options.silent || false | ||
settings.quiet = options.quiet || false | ||
settings.frail = options.frail || false | ||
settings.silent = options.silent | ||
settings.quiet = options.quiet | ||
settings.frail = options.frail | ||
/* Process. */ | ||
fileSetPipeline.run({files: options.files || []}, settings, next) | ||
// Process. | ||
fileSetPipeline.run({files: settings.files}, settings, next) | ||
function next(err, context) { | ||
var stats = statistics((context || {}).files) | ||
var failed = Boolean( | ||
/** | ||
* @param {Error | undefined} error | ||
* Error. | ||
* @param {Context | undefined} [context] | ||
* Context. | ||
* @returns {undefined} | ||
* Nothing. | ||
*/ | ||
function next(error, context) { | ||
const stats = statistics((context || {}).files || []) | ||
const failed = Boolean( | ||
settings.frail ? stats.fatal || stats.warn : stats.fatal | ||
) | ||
if (err) { | ||
callback(err) | ||
if (error) { | ||
callback(error) | ||
} else { | ||
callback(null, failed ? 1 : 0, context) | ||
callback(undefined, failed ? 1 : 0, context) | ||
} | ||
@@ -157,4 +403,18 @@ } | ||
function extension(ext) { | ||
return ext.charAt(0) === '.' ? ext : '.' + ext | ||
/** | ||
* | ||
* @param {unknown} value | ||
* Value. | ||
* @returns {value is URL} | ||
* Whether `value` is a URL. | ||
*/ | ||
function isUrl(value) { | ||
return ( | ||
value !== null && | ||
typeof value === 'object' && | ||
'href' in value && | ||
'searchParams' in value && | ||
// Extra, for vfiles. | ||
!('path' in value) | ||
) | ||
} |
159
package.json
{ | ||
"name": "unified-engine", | ||
"version": "6.0.1", | ||
"description": "Engine to process multiple files with unified", | ||
"version": "11.0.0", | ||
"description": "unified engine to process multiple files, lettings users configure from the file system", | ||
"license": "MIT", | ||
"keywords": [ | ||
"unified", | ||
"engine", | ||
"processor", | ||
"engine" | ||
"unified" | ||
], | ||
"repository": "unifiedjs/unified-engine", | ||
"bugs": "https://github.com/unifiedjs/unified-engine/issues", | ||
"author": "Titus Wormer <tituswormer@gmail.com> (http://wooorm.com)", | ||
"author": "Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)", | ||
"funding": { | ||
"type": "opencollective", | ||
"url": "https://opencollective.com/unified" | ||
}, | ||
"contributors": [ | ||
"Titus Wormer <tituswormer@gmail.com> (http://wooorm.com)" | ||
"Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)", | ||
"Christian Murphy <christian.murphy.42@gmail.com>" | ||
], | ||
"main": "lib/index.js", | ||
"sideEffects": false, | ||
"type": "module", | ||
"exports": "./index.js", | ||
"files": [ | ||
"lib/" | ||
"lib/", | ||
"index.d.ts", | ||
"index.js" | ||
], | ||
"dependencies": { | ||
"concat-stream": "^1.5.1", | ||
"debug": "^3.1.0", | ||
"fault": "^1.0.0", | ||
"fn-name": "^2.0.1", | ||
"glob": "^7.0.3", | ||
"ignore": "^3.2.0", | ||
"@types/concat-stream": "^2.0.0", | ||
"@types/debug": "^4.0.0", | ||
"@types/is-empty": "^1.0.0", | ||
"@types/node": "^20.0.0", | ||
"@types/unist": "^3.0.0", | ||
"@ungap/structured-clone": "^1.0.0", | ||
"concat-stream": "^2.0.0", | ||
"debug": "^4.0.0", | ||
"glob": "^10.0.0", | ||
"ignore": "^5.0.0", | ||
"is-empty": "^1.0.0", | ||
"is-hidden": "^1.0.1", | ||
"is-object": "^1.0.1", | ||
"js-yaml": "^3.6.1", | ||
"load-plugin": "^2.0.0", | ||
"parse-json": "^4.0.0", | ||
"to-vfile": "^4.0.0", | ||
"trough": "^1.0.0", | ||
"unist-util-inspect": "^4.1.2", | ||
"vfile-reporter": "^5.0.0", | ||
"vfile-statistics": "^1.1.0", | ||
"x-is-string": "^0.1.0", | ||
"xtend": "^4.0.1" | ||
"is-plain-obj": "^4.0.0", | ||
"load-plugin": "^5.0.0", | ||
"parse-json": "^7.0.0", | ||
"trough": "^2.0.0", | ||
"unist-util-inspect": "^8.0.0", | ||
"vfile": "^6.0.0", | ||
"vfile-message": "^4.0.0", | ||
"vfile-reporter": "^8.0.0", | ||
"vfile-statistics": "^3.0.0", | ||
"yaml": "^2.0.0" | ||
}, | ||
"devDependencies": { | ||
"nyc": "^11.0.0", | ||
"prettier": "^1.12.1", | ||
"remark-cli": "^5.0.0", | ||
"remark-preset-wooorm": "^4.0.0", | ||
"strip-ansi": "^4.0.0", | ||
"tape": "^4.4.0", | ||
"unified": "^7.0.0", | ||
"vfile-reporter-json": "^1.0.1", | ||
"vfile-reporter-pretty": "^1.0.1", | ||
"xo": "^0.21.0" | ||
"@types/parse-json": "^4.0.0", | ||
"@types/ungap__structured-clone": "^0.3.0", | ||
"c8": "^8.0.0", | ||
"prettier": "^3.0.0", | ||
"remark": "^14.0.0", | ||
"remark-cli": "^11.0.0", | ||
"remark-preset-wooorm": "^9.0.0", | ||
"remark-toc": "^8.0.0", | ||
"strip-ansi": "^7.0.0", | ||
"type-coverage": "^2.0.0", | ||
"typescript": "^5.0.0", | ||
"unified": "^11.0.0", | ||
"vfile-reporter-json": "^4.0.0", | ||
"vfile-reporter-pretty": "^7.0.0", | ||
"xo": "^0.56.0" | ||
}, | ||
"scripts": { | ||
"format": "remark . -qfo && prettier --write '**/*.js' && xo --fix", | ||
"test-api": "node test", | ||
"test-coverage": "nyc --reporter lcov tape test/index.js", | ||
"test": "npm run format && npm run test-coverage" | ||
"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 --100 --check-coverage --reporter lcov npm run test-api" | ||
}, | ||
"nyc": { | ||
"check-coverage": true, | ||
"lines": 100, | ||
"functions": 100, | ||
"branches": 100 | ||
"prettier": { | ||
"bracketSpacing": false, | ||
"singleQuote": true, | ||
"semi": false, | ||
"tabWidth": 2, | ||
"trailingComma": "none", | ||
"useTabs": false | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"preset-wooorm" | ||
"remark-preset-wooorm", | ||
[ | ||
"remark-toc", | ||
{ | ||
"heading": "contents", | ||
"maxDepth": 3, | ||
"tight": true | ||
} | ||
] | ||
] | ||
}, | ||
"prettier": { | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"singleQuote": true, | ||
"bracketSpacing": false, | ||
"semi": false, | ||
"trailingComma": "none" | ||
}, | ||
"xo": { | ||
"overrides": [ | ||
{ | ||
"files": "test/fixtures/**/*.js", | ||
"rules": { | ||
"unicorn/no-empty-file": "off", | ||
"unicorn/prefer-module": "off" | ||
} | ||
} | ||
], | ||
"prettier": true, | ||
"esnext": false, | ||
"rules": { | ||
"no-var": "off", | ||
"prefer-arrow-callback": "off", | ||
"object-shorthand": "off", | ||
"complexity": "off", | ||
"guard-for-in": "off" | ||
"no-await-in-loop": "off", | ||
"no-unused-expressions": "off", | ||
"unicorn/no-this-assignment": "off", | ||
"unicorn/prefer-at": "off", | ||
"unicorn/prefer-event-target": "off", | ||
"unicorn/prefer-string-replace-all": "off" | ||
} | ||
}, | ||
"typeCoverage": { | ||
"atLeast": 100, | ||
"detail": true, | ||
"ignoreCatch": true, | ||
"#": "needed `any`s", | ||
"ignoreFiles": [ | ||
"lib/index.d.ts", | ||
"lib/configuration.d.ts" | ||
], | ||
"strict": true | ||
} | ||
} |
1748
readme.md
@@ -1,26 +0,117 @@ | ||
# unified-engine [![Build Status][travis-badge]][travis] [![Coverage Status][codecov-badge]][codecov] | ||
# unified-engine | ||
Engine to process multiple files with [**unified**][unified], allowing users | ||
to [configure][] from the file-system. | ||
[![Build][build-badge]][build] | ||
[![Coverage][coverage-badge]][coverage] | ||
[![Downloads][downloads-badge]][downloads] | ||
[![Sponsors][sponsors-badge]][collective] | ||
[![Backers][backers-badge]][collective] | ||
[![Chat][chat-badge]][chat] | ||
## Projects | ||
**[unified][]** engine to process multiple files, lettings users | ||
[configure][config-files] from the file system. | ||
The following projects wrap the engine: | ||
## Contents | ||
* [`unified-args`][args] — Create CLIs for processors | ||
* [`unified-engine-gulp`][gulp] — Create Gulp plug-ins | ||
* [`unified-engine-atom`][atom] — Create Atom Linters for processors | ||
* [What is this?](#what-is-this) | ||
* [When should I use this?](#when-should-i-use-this) | ||
* [Install](#install) | ||
* [Use](#use) | ||
* [API](#api) | ||
* [`engine(options, callback)`](#engineoptions-callback) | ||
* [`Configuration`](#configuration) | ||
* [`Completer`](#completer) | ||
* [`Callback`](#callback) | ||
* [`ConfigTransform`](#configtransform) | ||
* [`Context`](#context) | ||
* [`FileSet`](#fileset) | ||
* [`Options`](#options) | ||
* [`Preset`](#preset) | ||
* [`ResolveFrom`](#resolvefrom) | ||
* [`VFileReporter`](#vfilereporter) | ||
* [Config files](#config-files) | ||
* [Explicit configuration](#explicit-configuration) | ||
* [Implicit configuration](#implicit-configuration) | ||
* [Examples](#examples) | ||
* [Ignore files](#ignore-files) | ||
* [Explicit ignoring](#explicit-ignoring) | ||
* [Implicit ignoring](#implicit-ignoring) | ||
* [Extra ignoring](#extra-ignoring) | ||
* [Ignoring](#ignoring) | ||
* [Examples](#examples-1) | ||
* [Plugins](#plugins) | ||
* [Examples](#examples-2) | ||
* [`options.alwaysStringify`](#optionsalwaysstringify) | ||
* [`options.configTransform`](#optionsconfigtransform) | ||
* [`options.defaultConfig`](#optionsdefaultconfig) | ||
* [`options.detectConfig`](#optionsdetectconfig) | ||
* [`options.detectIgnore`](#optionsdetectignore) | ||
* [`options.extensions`](#optionsextensions) | ||
* [`options.filePath`](#optionsfilepath) | ||
* [`options.files`](#optionsfiles) | ||
* [`options.frail`](#optionsfrail) | ||
* [`options.ignoreName`](#optionsignorename) | ||
* [`options.ignorePath`](#optionsignorepath) | ||
* [`options.ignorePathResolveFrom`](#optionsignorepathresolvefrom) | ||
* [`options.ignorePatterns`](#optionsignorepatterns) | ||
* [`options.ignoreUnconfigured`](#optionsignoreunconfigured) | ||
* [`options.inspect`](#optionsinspect) | ||
* [`options.out`](#optionsout) | ||
* [`options.output`](#optionsoutput) | ||
* [`options.packageField`](#optionspackagefield) | ||
* [`options.pluginPrefix`](#optionspluginprefix) | ||
* [`options.plugins`](#optionsplugins) | ||
* [`options.processor`](#optionsprocessor) | ||
* [`options.quiet`](#optionsquiet) | ||
* [`options.rcName`](#optionsrcname) | ||
* [`options.rcPath`](#optionsrcpath) | ||
* [`options.reporter` and `options.reporterOptions`](#optionsreporter-and-optionsreporteroptions) | ||
* [`options.settings`](#optionssettings) | ||
* [`options.silent`](#optionssilent) | ||
* [`options.streamError`](#optionsstreamerror) | ||
* [`options.streamIn`](#optionsstreamin) | ||
* [`options.streamOut`](#optionsstreamout) | ||
* [`options.tree`](#optionstree) | ||
* [`options.treeIn`](#optionstreein) | ||
* [`options.treeOut`](#optionstreeout) | ||
* [Types](#types) | ||
* [Compatibility](#compatibility) | ||
* [Security](#security) | ||
* [Contribute](#contribute) | ||
* [License](#license) | ||
## Installation | ||
## What is this? | ||
[npm][]: | ||
This package is the engine. | ||
It’s what you use underneath when you use [`remark-cli`][remark-cli] or a | ||
language server. | ||
Compared to unified, this deals with multiple files, often from the file | ||
system, and with [configuration files][config-files] and | ||
[ignore files][ignore-files]. | ||
```bash | ||
## When should I use this? | ||
You typically use something that wraps this, such as: | ||
* [`unified-args`][unified-args] | ||
— create CLIs | ||
* [`unified-engine-gulp`][unified-engine-gulp] | ||
— create Gulp plugins | ||
* [`unified-language-server`][unified-language-server] | ||
— create language servers | ||
You can use this to make such things. | ||
## Install | ||
This package is [ESM only][esm]. | ||
In Node.js (version 16+), install with [npm][]: | ||
```sh | ||
npm install unified-engine | ||
``` | ||
## Usage | ||
## Use | ||
The following example processes all files in the current directory with a | ||
markdown extension with [**remark**][remark], allows [configuration][configure] | ||
The following example processes all files in the current folder with a | ||
markdown extension with **[remark][]**, allows [configuration][config-files] | ||
from `.remarkrc` and `package.json` files, ignoring files from `.remarkignore` | ||
@@ -30,15 +121,20 @@ files, and more. | ||
```js | ||
var engine = require('unified-engine') | ||
var remark = require('remark') | ||
/** | ||
* @typedef {import('unified-engine').Callback} Callback | ||
*/ | ||
import process from 'node:process' | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
processor: remark, | ||
color: true, | ||
extensions: ['md', 'markdown', 'mkd', 'mkdn', 'mkdown'], | ||
files: ['.'], | ||
extensions: ['md', 'markdown', 'mkd', 'mkdn', 'mkdown'], | ||
ignoreName: '.remarkignore', | ||
packageField: 'remarkConfig', | ||
pluginPrefix: 'remark', | ||
rcName: '.remarkrc', | ||
packageField: 'remarkConfig', | ||
ignoreName: '.remarkignore', | ||
color: true | ||
processor: remark, | ||
rcName: '.remarkrc' | ||
}, | ||
@@ -48,134 +144,1416 @@ done | ||
function done(err) { | ||
if (err) throw err | ||
/** @type {Callback} */ | ||
function done(error, code) { | ||
if (error) throw error | ||
process.exitCode = code | ||
} | ||
``` | ||
## Table of Contents | ||
## API | ||
* [API](#api) | ||
* [engine(options, callback)](#engineoptions-callback) | ||
* [Plug-ins](#plug-ins) | ||
* [Configuration](#configuration) | ||
* [Ignoring](#ignoring) | ||
* [Contribute](#contribute) | ||
* [License](#license) | ||
This package exports the identifiers [`Configuration`][api-configuration] and | ||
[`engine`][api-engine]. | ||
There is no default export. | ||
## API | ||
### `engine(options, callback)` | ||
Process files according to `options` and invoke [`callback`][callback] when | ||
done. | ||
Process. | ||
###### [`options`][options] | ||
###### Parameters | ||
* [`processor`][processor] ([`Processor`][unified-processor]) | ||
— Unified processor to transform files | ||
* [`cwd`][cwd] (`string`, default: `process.cwd()`) | ||
— Directory to search files in, load plug-ins from, and more | ||
* [`files`][files] (`Array.<string|VFile>`, optional) | ||
— Paths or globs to files and directories, or virtual files, to process | ||
* [`extensions`][extensions] (`Array.<string>`, optional) | ||
— If `files` matches directories, include files with `extensions` | ||
* [`streamIn`][stream-in] (`ReadableStream`, default: `process.stdin`) | ||
— Stream to read from if no files are found or given | ||
* [`filePath`][file-path] (`string`, optional) | ||
— File path to process the given file on `streamIn` as | ||
* [`streamOut`][stream-out] (`WritableStream`, default: `process.stdout`) | ||
— Stream to write processed files to | ||
* [`streamError`][stream-error] (`WritableStream`, default: `process.stderr`) | ||
— Stream to write the report (if any) to | ||
* [`out`][out] (`boolean`, default: depends) | ||
— Whether to write the processed file to `streamOut` | ||
* [`output`][output] (`boolean` or `string`, default: `false`) | ||
— Whether to write successfully processed files, and where to | ||
* [`alwaysStringify`][always-stringify] (`boolean`, default: `false`) | ||
— Whether to always compile successfully processed files | ||
* [`tree`][tree] (`boolean`, default: `false`) | ||
— Whether to treat both input and output as a syntax tree | ||
* [`treeIn`][tree-in] (`boolean`, default: `tree`) | ||
— Whether to treat input as a syntax tree | ||
* [`treeOut`][tree-out] (`boolean`, default: `tree`) | ||
— Whether to treat output as a syntax tree | ||
* [`inspect`][inspect] (`boolean`, default: `false`) | ||
— Whether to output a formatted syntax tree | ||
* [`rcName`][rc-name] (`string`, optional) | ||
— Name of configuration files to load | ||
* [`packageField`][package-field] (`string`, optional) | ||
— Property at which configuration can be found in `package.json` files | ||
* [`detectConfig`][detect-config] (`boolean`, default: whether `rcName` or | ||
`packageField` is given) | ||
— Whether to search for configuration files | ||
* [`rcPath`][rc-path] (`string`, optional) | ||
— File-path to a configuration file to load | ||
* [`settings`][settings] (`Object`, optional) | ||
— Configuration for the parser and compiler of the processor | ||
* [`ignoreName`][ignore-name] (`string`, optional) | ||
— Name of ignore files to load | ||
* [`detectIgnore`][detect-ignore] (`boolean`, default: whether `ignoreName` | ||
is given) | ||
— Whether to search for ignore files | ||
* [`ignorePath`][ignore-path] (`string`, optional) | ||
— File-path to an ignore file to load | ||
* [`silentlyIgnore`][silently-ignore] (`boolean`, default: `false`) | ||
— Skip given files if they are ignored | ||
* [`plugins`][plugins] (`Array|Object`, optional) | ||
— Plug-ins to use | ||
* [`pluginPrefix`][plugin-prefix] (`string`, optional) | ||
— Optional prefix to use when searching for plug-ins | ||
* [`configTransform`][config-transform] (`Function`, optional) | ||
— Transform config files from a different schema | ||
* [`reporter`][reporter] (`string` or `function`, default: | ||
`require('vfile-reporter')`) | ||
— Reporter to use | ||
* [`reporterOptions`][reporteroptions] (`Object?`, optional) | ||
— Config to pass to the used reporter | ||
* [`color`][color] (`boolean`, default: `false`) | ||
— Whether to report with ANSI colour sequences | ||
* [`silent`][silent] (`boolean`, default: `false`) | ||
— Report only fatal errors | ||
* [`quiet`][quiet] (`boolean`, default: `silent`) | ||
— Do not report successful files | ||
* [`frail`][frail] (`boolean`, default: `false`) | ||
— Call back with an unsuccessful (`1`) code on warnings as well as errors | ||
* `options` ([`Options`][api-options], required) | ||
— configuration | ||
* `callback` ([`Callback`][api-callback], required) | ||
— configuration | ||
#### `function callback(err[, code, context])` | ||
###### Returns | ||
Callback invoked when processing according to `options` is complete. | ||
Invoked with either a fatal error if processing went horribly wrong | ||
(probably due to incorrect configuration), or a status code and the | ||
processing context. | ||
Nothing (`undefined`). | ||
### `Configuration` | ||
Internal class to load configuration files. | ||
Exposed to build more complex integrations. | ||
###### Parameters | ||
* `err` (`Error`) — Fatal error | ||
* `code` (`number`) — Either `0` if successful, or `1` if | ||
unsuccessful. The latter occurs if [fatal][] errors | ||
happen when processing individual files, or if [`frail`][frail] | ||
is set and warnings occur | ||
* `context` (`Object`) — Processing context, containing internally | ||
used information and a `files` array with the processed files | ||
* `options` (subset of [`Options`][api-options], required) | ||
— configuration (`cwd` is required) | ||
## Plug-ins | ||
###### Fields | ||
[`doc/plug-ins.md`][plug-ins] describes in detail how plug-ins | ||
can add more files to be processed and handle all transformed files. | ||
* `load(string, (Error?[, Result?]): undefined): undefined` | ||
— get the config for a file | ||
## Configuration | ||
### `Completer` | ||
[`doc/configure.md`][configure] describes in detail how configuration | ||
files work. | ||
Completer (TypeScript type). | ||
## Ignoring | ||
###### Type | ||
[`doc/ignore.md`][ignore] describes in detail how ignore files work. | ||
```ts | ||
type Completer = (CompleterCallback | CompleterRegular) & { | ||
pluginId?: string | symbol | undefined | ||
} | ||
type CompleterCallback = (set: FileSet, next: CompleterCallbackNext) => undefined | ||
type CompleterCallbackNext = (error?: Error | null | undefined) => undefined | ||
type CompleterRegular = (set: FileSet) => Promise<undefined> | undefined | ||
``` | ||
### `Callback` | ||
Callback called when done (TypeScript type). | ||
Called with a fatal error if things went horribly wrong (probably due to | ||
incorrect configuration), or a status code and the processing context. | ||
###### Parameters | ||
* `error` (`Error`, optional) | ||
— error | ||
* `code` (`0` or `1`, optional) | ||
— exit code, `0` if successful or `1` if unsuccessful | ||
* `context` ([`Context`][api-context], optional) | ||
— processing context | ||
###### Returns | ||
Nothing (`undefined`). | ||
### `ConfigTransform` | ||
Transform arbitrary configs to our format (TypeScript type). | ||
###### Parameters | ||
* `config` (`unknown`) | ||
— arbitrary config | ||
* `filePath` (`string`) | ||
— file path of config file | ||
###### Returns | ||
Our config format ([`Preset`][api-preset]). | ||
### `Context` | ||
Processing context (TypeScript type). | ||
###### Fields | ||
* `fileSet` ([`FileSet`][api-file-set]) | ||
— internally used info | ||
* `files` ([`Array<VFile>`][vfile]) | ||
— processed files | ||
### `FileSet` | ||
A FileSet is created to process multiple files through unified processors | ||
(TypeScript type). | ||
This set, containing all files, is exposed to plugins as the second parameter. | ||
###### Parameters | ||
None. | ||
###### Fields | ||
* `valueOf(): Array<VFile>` | ||
— get files in a set | ||
* `use(completer: Completer): this` | ||
— add middleware to be called when done (see: [`Completer`][api-completer]) | ||
* `add(file: VFile | string): this` | ||
— add a file; the given file is processed like other files with a few | ||
differences: it’s ignored when their file path is already added, never | ||
written to the file system or `streamOut`, and not included in the report | ||
### `Options` | ||
Configuration (TypeScript type). | ||
> 👉 **Note**: `options.processor` is required. | ||
###### Fields | ||
* `alwaysStringify` (`boolean`, default: `false`) | ||
— whether to always serialize successfully processed files | ||
* `color` (`boolean`, default: `false`) | ||
— whether to report with ANSI color sequences; given to the reporter | ||
* `configTransform` ([`ConfigTransform`][api-config-transform], optional) | ||
— transform config files from a different schema | ||
* `cwd` (`URL` or `string`, default: `process.cwd()`) | ||
— folder to search files in, load plugins from, and more | ||
* `defaultConfig` ([`Preset`][api-preset], optional) | ||
— default configuration to use if no config file is given or found | ||
* `detectConfig` (`boolean`, default: `true` if `options.packageField` or | ||
`options.rcName`) | ||
— whether to search for configuration files | ||
* `detectIgnore` (`boolean`, default: `true` if `options.ignoreName`) | ||
— whether to search for ignore files | ||
* `extensions` (`Array<string>`, optional) | ||
— search for files with these extensions, when folders are passed; | ||
generated files are also given the first extension if `treeIn` is on and | ||
`output` is on or points to a folder | ||
* `filePath` (`URL` or `string`, optional) | ||
— file path to process the given file on `streamIn` as | ||
* `files` (`Array<URL | VFile | string>`, optional) | ||
— paths or [globs][node-glob] to files and folder, or virtual files, to | ||
process | ||
* `frail` (`boolean`, default: `false`) | ||
— call back with an unsuccessful (`1`) code on warnings as well as errors | ||
* `ignoreName` (`string`, optional) | ||
— name of ignore files to load | ||
* `ignorePath` (`URL` or `string`, optional) | ||
— filepath to an ignore file to load | ||
* `ignorePathResolveFrom` ([`ResolveFrom`][api-resolve-from], default: | ||
`'dir'`) | ||
— resolve patterns in `ignorePath` from the current working folder | ||
(`'cwd'`) or the ignore file’s folder (`'dir'`) | ||
* `ignorePatterns` (optional) | ||
— patterns to ignore in addition to ignore files | ||
* `ignoreUnconfigured` (`boolean`, default: `false`) | ||
— ignore files that do not have an associated detected configuration file; | ||
either `rcName` or `packageField` must be defined too; cannot be combined | ||
with `rcPath` or `detectConfig: false` | ||
* `inspect` (`boolean`, default: `false`) | ||
— whether to output a formatted syntax tree for debugging | ||
* `out` (`boolean`, default: `false`) | ||
— whether to write the processed file to `streamOut` | ||
* `output` (`URL`, `boolean` or `string`, default: `false`) | ||
— whether to write successfully processed files, and where to; when `true`, | ||
overwrites the given files, when `false`, does not write to the file system; | ||
when pointing to an existing folder, files are written to that folder and | ||
keep their original basenames; when the parent folder of the given path | ||
exists and one file is processed, the file is written to the given path | ||
* `packageField` (`string`, optional) | ||
— field where configuration can be found in `package.json` files | ||
* `pluginPrefix` (`string`, optional) | ||
— prefix to use when searching for plugins | ||
* `plugins` ([`Preset['plugins']`][api-preset], optional) | ||
— plugins to use | ||
* `processor` ([`Processor`][unified-processor], **required**) | ||
— unified processor to transform files | ||
* `quiet` (`boolean`, default: `false`) | ||
— do not report successful files; given to the reporter | ||
* `rcName` (`string`, optional) | ||
— name of configuration files to load | ||
* `rcPath` (`URL` or `string`, optional) | ||
— filepath to a configuration file to load | ||
* `reporter` ([`VFileReporter`][api-vfile-reporter] or `string`, default: | ||
`vfile-reporter`) | ||
— reporter to use; if a `string` is passed, it’s loaded from `cwd`, and | ||
`'vfile-reporter-'` can be omitted | ||
* `reporterOptions` ([`Options`][vfile-reporter-options] from | ||
`vfile-reporter`, optional) | ||
— config to pass to the used reporter | ||
* `settings` ([`Settings`][unified-settings] from `unified`, optional) | ||
— configuration for the parser and compiler of the processor | ||
* `silent` (`boolean`, default: `false`) | ||
— report only fatal errors; given to the reporter | ||
* `silentlyIgnore` (`boolean`, default: `false`) | ||
— skip given files if they are ignored | ||
* `streamError` ([`WritableStream`][node-writable-stream] from Node.js, | ||
default: `process.stderr`) | ||
— stream to write the report (if any) to | ||
* `streamIn` ([`ReadableStream`][node-readable-stream] from Node.js, | ||
default: `process.stdin`) | ||
— stream to read from if no files are found or given | ||
* `streamOut` ([`WritableStream`][node-writable-stream] from Node.js, | ||
default: `process.stdout`) | ||
— stream to write processed files to, nothing is streamed if either `out` | ||
is `false`, `output` is not `false`, multiple files are processed, or a | ||
fatal error occurred while processing a file | ||
* `tree` (`boolean`, default: `false`) | ||
— whether to treat both input and output as a syntax tree | ||
* `treeIn` (`boolean`, default: `options.tree`) | ||
— whether to treat input as a syntax tree | ||
* `treeOut` (`boolean`, default: `options.tree`) | ||
— whether to output as a syntax tree | ||
### `Preset` | ||
Sharable configuration, with support for specifiers (TypeScript type). | ||
Specifiers should *not* be used in actual presets (because they can’t be | ||
used by regular unified), but they can be used in config files locally, | ||
as those are only for the engine. | ||
They can contain plugins and settings. | ||
###### Type | ||
```ts | ||
import type { | ||
Plugin as UnifiedPlugin, | ||
PluginTuple as UnifiedPluginTuple, | ||
Preset as UnifiedPreset, | ||
Settings | ||
} from 'unified' | ||
type Preset = { | ||
plugins?: PluggableList | PluggableMap | undefined | ||
settings?: Settings | undefined | ||
} | ||
type Pluggable = Plugin | PluginTuple | UnifiedPreset | ||
type PluggableList = Array<Pluggable> | ||
type PluggableMap = Record<string, unknown> | ||
type Plugin = UnifiedPlugin | string | ||
type PluginTupleSupportingSpecifiers = | ||
| [plugin: string, ...parameters: Array<unknown>] | ||
| UnifiedPluginTuple | ||
``` | ||
### `ResolveFrom` | ||
How to resolve (TypeScript type). | ||
###### Type | ||
```ts | ||
type ResolveFrom = 'cwd' | 'dir'; | ||
``` | ||
### `VFileReporter` | ||
Transform arbitrary configs to our format (TypeScript type). | ||
This is essentially the interface of [`vfile-reporter`][vfile-reporter], with | ||
added support for unknown fields in options and async support. | ||
###### Parameters | ||
* `files` ([`Array<VFile>`][vfile]) | ||
— files | ||
* `options` ([`Options`][vfile-reporter-options] from `vfile-reporter`, | ||
optional) | ||
— configuration | ||
###### Returns | ||
Report (`Promise<string>` or `string`). | ||
## Config files | ||
`unified-engine` accepts configuration through options and through | ||
configuration files (*rc files*). | ||
### Explicit configuration | ||
One configuration file can be given through `options.rcPath`, this is loaded | ||
regardless of `options.detectConfig` and `options.rcName`. | ||
### Implicit configuration | ||
Otherwise, configuration files are detected if `options.detectConfig` is turned | ||
on, depending on the following options: | ||
* if `options.rcName` is given, `$rcName` (JSON), `$rcName.js` (CommonJS or | ||
ESM depending on the `type` field of the closest `package.json`), | ||
`$rcName.cjs` (CommonJS), `$rcName.mjs` (ESM), `$rcName.yml` (YAML), | ||
and `$rcName.yaml` (YAML) are loaded | ||
* if `options.packageField` is given, `package.json` (JSON) files are loaded | ||
and the configuration at their `$packageField` field is used | ||
The first file that is searched for in a folder is used as the configuration. | ||
If no file is found, the parent folder is searched, and so on. | ||
The schema (type) of rc files is [`Preset`][api-preset]. | ||
### Examples | ||
An example **rc** file could look as follows: | ||
```json | ||
{ | ||
"plugins": [ | ||
"remark-inline-links", | ||
"remark-lint-recommended" | ||
], | ||
"settings": { | ||
"bullet": "*", | ||
"ruleRepetition": 3, | ||
"fences": true | ||
} | ||
} | ||
``` | ||
Another example, **rc.js**, could look as follows: | ||
```js | ||
exports.plugins = [ | ||
'./script/natural-language.js', | ||
'remark-lint-recommended', | ||
'remark-license' | ||
] | ||
exports.settings = {bullet: '*'} | ||
``` | ||
When using ESM (ECMAScript modules), **rc.mjs** could look as folows: | ||
```js | ||
export default { | ||
plugins: [ | ||
'./script/natural-language.js', | ||
'remark-lint-recommended', | ||
'remark-license' | ||
], | ||
settings: {bullet: '*'} | ||
} | ||
``` | ||
Another example, **rc.yaml**, could look as follows: | ||
```js | ||
plugins: | ||
- 'rehype-document' | ||
- 'rehype-preset-minify' | ||
settings: | ||
preferUnquoted: true | ||
quote: "'" | ||
quoteSmart: true | ||
verbose: true | ||
``` | ||
## Ignore files | ||
`unified-engine` accepts patterns to ignore when searching for files to process | ||
through ignore files. | ||
### Explicit ignoring | ||
One ignore file can be given through `options.ignorePath`, this is loaded | ||
regardless of `options.detectIgnore` and `options.ignoreName`. | ||
### Implicit ignoring | ||
Otherwise, ignore files are detected if `options.detectIgnore` is turned on and | ||
`options.ignoreName` is given. | ||
The first file named `$ignoreName` in the parent folder of a checked file is | ||
used. | ||
Or, if no file is found, the parent folder if searched, and so on. | ||
### Extra ignoring | ||
In addition to explicit and implicit ignore files, other patterns can be given | ||
with `options.ignorePatterns`. | ||
The format of each pattern in `ignorePatterns` is the same as a line in an | ||
ignore file. | ||
Patterns and files are resolved based on the current working folder. | ||
It is also possible to ignore files that do not have an associated detected | ||
configuration file by turning on `options.ignoreUnconfigured`. | ||
### Ignoring | ||
Ignoring is used when searching for files in folders. | ||
If paths (including those expanded from [globs][node-glob]) are passed in that | ||
are ignored, an error is thrown. | ||
These files can be silently ignored by turning on `options.silentlyIgnore`. | ||
Normally, files are ignored based on the path of the found ignore file and the | ||
patterns inside it. | ||
Patterns passed with `options.ignorePatterns` are resolved based on the current | ||
working directory. | ||
Patterns in an explicit ignore file passed in with `options.ignorePath` can be | ||
resolved from the current working directory instead, by setting | ||
`options.ignorePathResolveFrom` to `'cwd'` instead of `'dir'` (default). | ||
If paths or globs to folders are given to the engine, they will be searched | ||
for matching files, but `node_modules` are normally not searched. | ||
Pass paths (or globs) to the `node_modules` you want to include in | ||
`options.files` to search them. | ||
The format for ignore files is the same as [`.gitignore`][gitignore], so it’s | ||
possible to pass a `.gitignore` in as `options.ignorePath`. | ||
[`node-ignore`][node-ignore] is used under the hood, see its documentation | ||
for more information. | ||
### Examples | ||
An example **ignore** file could look as follows: | ||
```ini | ||
# Ignore files in `.github`. | ||
.github/ | ||
# Bower. | ||
bower_components/ | ||
# Duo dependencies. | ||
components/ | ||
# Fixtures. | ||
test/{input,tree}/ | ||
``` | ||
If we had an ignore file `folder/.remarkignore`, with the value: `index.txt`, | ||
and our file system looked as follows: | ||
```txt | ||
folder/.remarkignore | ||
folder/index.txt | ||
index.txt | ||
``` | ||
Then `folder/index.txt` would be ignored but `index.txt` would not be. | ||
## Plugins | ||
Normally, **unified** plugins receive a single `options` argument upon attaching | ||
(an `Object` users can provide to configure the plugin). | ||
If a plugin is attached by **unified-engine**, a second argument is given: | ||
[`FileSet`][api-file-set]. | ||
## Examples | ||
`unified-engine` can be configured extensively by engine authors. | ||
### `options.alwaysStringify` | ||
This example shows how you can use `options.alwaysStringify` when you don’t | ||
want the engine to write to the file system, but still want to get the compiled | ||
results. | ||
One example that does this is `unified-engine-gulp`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
import {VFile} from 'vfile' | ||
const file = new VFile({path: 'example.md', value: '_hi_'}) | ||
engine( | ||
{alwaysStringify: true, files: [file], processor: remark}, | ||
function (error, code, context) { | ||
if (error) throw error | ||
console.log(context?.files.map((d) => String(d))) | ||
} | ||
) | ||
``` | ||
Yields: | ||
```txt | ||
example.md: no issues found | ||
``` | ||
```js | ||
[ '*hi*\n' ] | ||
``` | ||
### `options.configTransform` | ||
To support custom rc files, that have a different format than what the engine | ||
supports, pass as [`ConfigTransform`][api-config-transform]. | ||
This example processes `readme.md` and loads options from `custom` (from a | ||
`package.json`). | ||
`configTransform` is called with those options and transforms it to | ||
configuration `unified-engine` understands. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
configTransform, | ||
files: ['readme.md'], | ||
packageField: 'custom', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
function configTransform(config) { | ||
return {settings: (config || {}).options} | ||
} | ||
``` | ||
Where `package.json` contains: | ||
```json | ||
{ | ||
"name": "foo", | ||
"private": true, | ||
"custom": { | ||
"options": { | ||
"bullet": "+" | ||
} | ||
} | ||
} | ||
``` | ||
### `options.defaultConfig` | ||
This example processes `readme.md`. | ||
If `package.json` exists, that config is used, otherwise the configuration at | ||
`defaultConfig` is used. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
defaultConfig: {settings: {bullet: '+'}}, | ||
files: ['readme.md'], | ||
packageField: 'remarkConfig', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
Where `package.json` contains: | ||
```json | ||
{ | ||
"name": "foo", | ||
"private": true, | ||
"remarkConfig": { | ||
"settings": { | ||
"bullet": "-" | ||
} | ||
} | ||
} | ||
``` | ||
### `options.detectConfig` | ||
This example processes `readme.md` but does **not** allow configuration from | ||
`.remarkrc` or `package.json` files, as `detectConfig` is `false`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
detectConfig: false, | ||
files: ['readme.md'], | ||
processor: remark(), | ||
packageField: 'remarkConfig', | ||
rcName: '.remarkrc' | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.detectIgnore` | ||
This example processes files in the current working directory with an `md` | ||
extension but does **not** ignore file paths from the closest `.remarkignore` | ||
file, because `detectIgnore` is `false`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
detectIgnore: false, | ||
extensions: ['md'], | ||
files: ['.'], | ||
ignoreName: '.remarkignore', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.extensions` | ||
This example reformats all files with `md`, `markdown`, and `mkd` | ||
extensions in the current folder. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md', 'mkd', 'markdown'], | ||
files: ['.'], | ||
output: true, | ||
processor: remark | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.filePath` | ||
This example shows that `streamIn` is named as `filePath`: | ||
```js | ||
import {PassThrough} from 'node:stream' | ||
import {remark} from 'remark' | ||
import remarkPresetLintRecommended from 'remark-preset-lint-recommended' | ||
import {engine} from 'unified-engine' | ||
const streamIn = new PassThrough() | ||
streamIn.write('doc') | ||
setImmediate(function () { | ||
streamIn.end('ument') | ||
}) | ||
engine( | ||
{ | ||
filePath: '~/alpha/bravo/charlie.md', | ||
out: false, | ||
plugins: [remarkPresetLintRecommended], | ||
processor: remark(), | ||
streamIn | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
Yields: | ||
```txt | ||
~/alpha/bravo/charlie.md | ||
1:1 warning Missing newline character at end of file final-newline remark-lint | ||
⚠ 1 warning | ||
``` | ||
### `options.files` | ||
This example processes `LICENSE` and all files with an `md` extension in `doc`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md'], | ||
files: ['LICENSE', 'doc/'], | ||
processor: remark | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.frail` | ||
This example uses [`remark-lint`][remark-lint] to lint `readme.md` and exits | ||
with the given exit code. | ||
Normally, only errors turn the `code` to `1`, but in `frail` mode lint warnings | ||
result in the same. | ||
```js | ||
import process from 'node:process' | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
frail: true, | ||
plugins: ['remark-preset-lint-recommended'], | ||
processor: remark() | ||
}, | ||
function (error, code) { | ||
process.exitCode = error ? 1 : code | ||
} | ||
) | ||
``` | ||
### `options.ignoreName` | ||
This example processes files in the current working directory with an `md` | ||
extension, and is configured to ignore file paths from the closest | ||
`.remarkignore` file. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md'], | ||
files: ['.'], | ||
ignoreName: '.remarkignore', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.ignorePath` | ||
This example processes files in the current working directory with an `md` | ||
extension and ignores file paths specified in `.gitignore`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md'], | ||
files: ['.'], | ||
ignorePath: '.gitignore', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.ignorePathResolveFrom` | ||
This example processes files in the current working directory with an `md` | ||
extension and takes a reusable configuration file from a dependency. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md'], | ||
files: ['.'], | ||
ignorePath: 'node_modules/my-config/my-ignore', | ||
ignorePathResolveFrom: 'cwd', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.ignorePatterns` | ||
This example processes files in the current working directory with an `md` | ||
extension, except for `readme.md`: | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md'], | ||
files: ['.'], | ||
ignorePatterns: ['readme.md'], | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.ignoreUnconfigured` | ||
This example processes files in the current working directory with an | ||
`md` extension, but only if there is an explicit `.remarkrc` config file near | ||
(upwards) to them: | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md'], | ||
files: ['.'], | ||
ignoreUnconfigured: true, | ||
processor: remark(), | ||
rcName: '.remarkrc' | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.inspect` | ||
This example shows a module which reads and parses `doc.md`, then | ||
[`remark-unlink`][remark-unlink] transforms the syntax tree, the tree is | ||
formatted with [`unist-util-inspect`][unist-util-inspect], and finally written | ||
to **stdout**(4). | ||
```js | ||
import {remark} from 'remark' | ||
import remarkUnlink from 'remark-unlink' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['doc.md'], | ||
inspect: true, | ||
plugins: [remarkUnlink], | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
Where `doc.md` looks as follows: | ||
```markdown | ||
[foo](https://example.com) | ||
``` | ||
Yields: | ||
```txt | ||
root[1] (1:1-2:1, 0-27) | ||
└─ paragraph[1] (1:1-1:27, 0-26) | ||
└─ text: "foo" (1:2-1:5, 1-4) | ||
``` | ||
### `options.out` | ||
This example uses [`remark-lint`][remark-lint] to lint `readme.md`, writes the | ||
report, and ignores the serialized document. | ||
```js | ||
import {remark} from 'remark' | ||
import remarkPresetLintRecommended from 'remark-preset-lint-recommended' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
out: false, | ||
plugins: [remarkPresetLintRecommended], | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.output` | ||
This example writes all files in `src/` with an `md` extension compiled to | ||
`dest/`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['md'], | ||
files: ['src/'], | ||
output: 'dest/', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.packageField` | ||
This example processes `readme.md`, and allows configuration from | ||
`remarkConfig` fields in `package.json` files. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
packageField: 'remarkConfig', | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.pluginPrefix` | ||
This example processes `readme.md` and loads the | ||
`preset-lint-recommended` plugin. | ||
Because `pluginPrefix` is given, this resolves to | ||
[`remark-preset-lint-recommended`][remark-preset-lint-recommended] (from | ||
`node_modules/`) if available. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
pluginPrefix: 'remark', | ||
plugins: ['preset-lint-recommended'], | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.plugins` | ||
This example processes `readme.md` and loads the | ||
[`remark-preset-lint-recommended`][remark-preset-lint-recommended] | ||
preset. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
plugins: ['remark-preset-lint-recommended'], | ||
processor: remark() | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.processor` | ||
This example reformats **stdin**(4) using [remark][], writes the report | ||
to **stderr**(4), and formatted document to **stdout**(4). | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine({processor: remark}, function (error) { | ||
if (error) throw error | ||
}) | ||
``` | ||
### `options.quiet` | ||
This example uses [`remark-lint`][remark-lint] to lint `readme.md`. | ||
Nothing is reported if the file processed successfully. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
plugins: ['remark-preset-lint-recommended'], | ||
processor: remark(), | ||
quiet: true | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.rcName` | ||
This example processes `readme.md` and allows configuration from `.remarkrc`, | ||
`.remarkrc.json`, `.remarkrc.yml`, `.remarkrc.yaml`, `.remarkrc.js`, | ||
`.remarkrc.cjs`, and `.remarkrc.mjs` files. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{files: ['readme.md'], processor: remark(), rcName: '.remarkrc'}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.rcPath` | ||
This example processes `readme.md` and loads configuration from `config.json`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{files: ['readme.md'], processor: remark(), rcPath: 'config.json'}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.reporter` and `options.reporterOptions` | ||
This example processes all HTML files in the current folder with rehype, | ||
configures the processor with `.rehyperc` files, and prints a report in | ||
JSON using [`vfile-reporter-json`][vfile-reporter-json] with | ||
[reporter options][vfile-reporter-options]. | ||
```js | ||
import {rehype} from 'rehype' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
extensions: ['html'], | ||
files: ['.'], | ||
processor: rehype(), | ||
rcName: '.rehyperc', | ||
reporter: 'json', | ||
reporterOptions: {pretty: true} | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.settings` | ||
This example processes `readme.md` and configures the compiler | ||
([`remark-stringify`][remark-stringify]) with `bullet: '+'`. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{files: ['readme.md'], processor: remark(), settings: {bullet: '+'}}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.silent` | ||
This example uses [`remark-lint`][remark-lint] to lint `readme.md` but does not | ||
report any warnings or success messages, only fatal errors, if they occur. | ||
```js | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
plugins: ['remark-preset-lint-recommended'], | ||
processor: remark(), | ||
silent: true | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.streamError` | ||
This example uses [`remark-lint`][remark-lint] to lint `readme.md` and writes | ||
the report to `report.txt`. | ||
```js | ||
import fs from 'node:fs' | ||
import {remark} from 'remark' | ||
import remarkPresetLintRecommended from 'remark-preset-lint-recommended' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['readme.md'], | ||
out: false, | ||
plugins: [remarkPresetLintRecommended], | ||
processor: remark(), | ||
streamErr: fs.createWriteStream('report.txt') | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.streamIn` | ||
This example uses [`remark-lint`][remark-lint] to lint an incoming | ||
stream. | ||
```js | ||
import {PassThrough} from 'node:stream' | ||
import {remark} from 'remark' | ||
import remarkPresetLintRecommended from 'remark-preset-lint-recommended' | ||
import {engine} from 'unified-engine' | ||
const streamIn = new PassThrough() | ||
streamIn.write('doc') | ||
setImmediate(function () { | ||
streamIn.end('ument') | ||
}) | ||
engine( | ||
{ | ||
out: false, | ||
plugins: [remarkPresetLintRecommended], | ||
processor: remark(), | ||
streamIn | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
Yields: | ||
```txt | ||
<stdin> | ||
1:1 warning Missing newline character at end of file final-newline remark-lint | ||
⚠ 1 warning | ||
``` | ||
### `options.streamOut` | ||
This example reads `readme.md` and writes the serialized document to | ||
`readme-two.md`. | ||
This can also be achieved by passing `output: 'readme-two.md'` instead of | ||
`streamOut`. | ||
```js | ||
import fs from 'node:fs' | ||
import {remark} from 'remark' | ||
import {engine} from 'unified-engine' | ||
const streamOut = fs.createWriteStream('readme-two.md') | ||
engine( | ||
{files: ['readme.md'], processor: remark(), streamOut}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
### `options.tree` | ||
This example reads `tree.json`, then [`remark-unlink`][remark-unlink] | ||
transforms the syntax tree, and the transformed tree is written to | ||
**stdout**(4). | ||
```js | ||
import {remark} from 'remark' | ||
import remarkUnlink from 'remark-unlink' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['tree.json'], | ||
plugins: [remarkUnlink], | ||
processor: remark(), | ||
tree: true | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
Where `tree.json` looks as follows: | ||
```json | ||
{ | ||
"type": "paragraph", | ||
"children": [{ | ||
"type": "link", | ||
"url": "https://example.com", | ||
"children": [{ | ||
"type": "text", | ||
"value": "foo" | ||
}] | ||
}] | ||
} | ||
``` | ||
Yields: | ||
```json | ||
{ | ||
"type": "paragraph", | ||
"children": [{ | ||
"type": "text", | ||
"value": "foo" | ||
}] | ||
} | ||
``` | ||
### `options.treeIn` | ||
This example reads `tree.json`, then [`remark-unlink`][remark-unlink] | ||
transforms the syntax tree, the tree is serialized, and the resulting document | ||
is written to **stdout**(4). | ||
```js | ||
import {remark} from 'remark' | ||
import remarkUnlink from 'remark-unlink' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['tree.json'], | ||
plugins: [remarkUnlink], | ||
processor: remark(), | ||
treeIn: true | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
Where `tree.json` looks as follows: | ||
```json | ||
{ | ||
"type": "paragraph", | ||
"children": [{ | ||
"type": "link", | ||
"url": "https://example.com", | ||
"children": [{ | ||
"type": "text", | ||
"value": "foo" | ||
}] | ||
}] | ||
} | ||
``` | ||
Yields: | ||
```markdown | ||
foo | ||
``` | ||
### `options.treeOut` | ||
This example shows a module which reads and parses `doc.md`, then | ||
[`remark-unlink`][remark-unlink] transforms the syntax tree, and the tree is | ||
written to **stdout**(4). | ||
```js | ||
import {remark} from 'remark' | ||
import remarkUnlink from 'remark-unlink' | ||
import {engine} from 'unified-engine' | ||
engine( | ||
{ | ||
files: ['doc.md'], | ||
plugins: [remarkUnlink], | ||
processor: remark(), | ||
treeOut: true | ||
}, | ||
function (error) { | ||
if (error) throw error | ||
} | ||
) | ||
``` | ||
Where `doc.md` looks as follows: | ||
```markdown | ||
[foo](https://example.com) | ||
``` | ||
Yields: | ||
```json | ||
{ | ||
"type": "paragraph", | ||
"children": [{ | ||
"type": "text", | ||
"value": "foo" | ||
}] | ||
} | ||
``` | ||
## Types | ||
This package is fully typed with [TypeScript][]. | ||
It exports the additional types | ||
[`Completer`][api-completer], | ||
[`Callback`][api-callback], | ||
[`ConfigTransform`][api-config-transform], | ||
[`Context`][api-context], | ||
[`FileSet`][api-file-set], | ||
[`Options`][api-options], | ||
[`Preset`][api-preset], | ||
[`ResolveFrom`][api-resolve-from], and | ||
[`VFileReporter`][api-vfile-reporter]. | ||
## Compatibility | ||
Projects maintained by the unified collective are compatible with maintained | ||
versions of Node.js. | ||
When we cut a new major release, we drop support for unmaintained versions of | ||
Node. | ||
This means we try to keep the current release line, `unified-engine@^11`, | ||
compatible with Node.js 16. | ||
## Security | ||
`unified-engine` loads and evaluates configuration files, plugins, and presets | ||
from the file system (often from `node_modules/`). | ||
That means code that is on your file system runs. | ||
Make sure you trust the workspace where you run `unified-engine` and be careful | ||
with packages from npm and changes made by contributors. | ||
## Contribute | ||
See [`contributing.md` in `unifiedjs/unified`][contributing] for ways to get | ||
started. | ||
See [`contributing.md`][contributing] in [`unifiedjs/.github`][health] for ways | ||
to get started. | ||
See [`support.md`][support] for ways to get help. | ||
This organisation has a [Code of Conduct][coc]. By interacting with this | ||
repository, organisation, or community you agree to abide by its terms. | ||
This project has a [code of conduct][coc]. | ||
By interacting with this repository, organization, or community you agree to | ||
abide by its terms. | ||
@@ -188,108 +1566,110 @@ ## License | ||
[travis-badge]: https://img.shields.io/travis/unifiedjs/unified-engine.svg | ||
[build-badge]: https://github.com/unifiedjs/unified-engine/workflows/main/badge.svg | ||
[travis]: https://travis-ci.org/unifiedjs/unified-engine | ||
[build]: https://github.com/unifiedjs/unified-engine/actions | ||
[codecov-badge]: https://img.shields.io/codecov/c/github/unifiedjs/unified-engine.svg | ||
[coverage-badge]: https://img.shields.io/codecov/c/github/unifiedjs/unified-engine.svg | ||
[codecov]: https://codecov.io/github/unifiedjs/unified-engine | ||
[coverage]: https://codecov.io/github/unifiedjs/unified-engine | ||
[npm]: https://docs.npmjs.com/cli/install | ||
[downloads-badge]: https://img.shields.io/npm/dm/unified-engine.svg | ||
[license]: LICENSE | ||
[downloads]: https://www.npmjs.com/package/unified-engine | ||
[author]: http://wooorm.com | ||
[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg | ||
[unified]: https://github.com/unifiedjs/unified | ||
[backers-badge]: https://opencollective.com/unified/backers/badge.svg | ||
[unified-processor]: https://github.com/unifiedjs/unified#processor | ||
[collective]: https://opencollective.com/unified | ||
[remark]: https://github.com/remarkjs/remark | ||
[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg | ||
[fatal]: https://github.com/vfile/vfile#vfilefailreason-position-ruleid | ||
[chat]: https://github.com/unifiedjs/unified/discussions | ||
[callback]: #function-callbackerr-code-context | ||
[npm]: https://docs.npmjs.com/cli/install | ||
[options]: doc/options.md#options | ||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c | ||
[processor]: doc/options.md#optionsprocessor | ||
[typescript]: https://www.typescriptlang.org | ||
[cwd]: doc/options.md#optionscwd | ||
[health]: https://github.com/unifiedjs/.github | ||
[extensions]: doc/options.md#optionsextensions | ||
[contributing]: https://github.com/unifiedjs/.github/blob/main/contributing.md | ||
[stream-in]: doc/options.md#optionsstreamin | ||
[support]: https://github.com/unifiedjs/.github/blob/main/support.md | ||
[file-path]: doc/options.md#optionsfilepath | ||
[coc]: https://github.com/unifiedjs/.github/blob/main/code-of-conduct.md | ||
[stream-out]: doc/options.md#optionsstreamout | ||
[license]: license | ||
[stream-error]: doc/options.md#optionsstreamerror | ||
[author]: https://wooorm.com | ||
[out]: doc/options.md#optionsout | ||
[gitignore]: https://git-scm.com/docs/gitignore | ||
[output]: doc/options.md#optionsoutput | ||
[node-glob]: https://github.com/isaacs/node-glob#glob-primer | ||
[always-stringify]: doc/options.md#optionsalwaysstringify | ||
[node-ignore]: https://github.com/kaelzhang/node-ignore | ||
[tree]: doc/options.md#optionstree | ||
[remark]: https://github.com/remarkjs/remark | ||
[tree-in]: doc/options.md#optionstreein | ||
[remark-cli]: https://github.com/remarkjs/remark/tree/main/packages/remark-cli#readme | ||
[tree-out]: doc/options.md#optionstreeout | ||
[remark-lint]: https://github.com/remarkjs/remark-lint | ||
[inspect]: doc/options.md#optionsinspect | ||
[remark-preset-lint-recommended]: https://github.com/remarkjs/remark-lint/tree/main/packages/remark-preset-lint-recommended | ||
[detect-config]: doc/options.md#optionsdetectconfig | ||
[remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify | ||
[rc-name]: doc/options.md#optionsrcname | ||
[remark-unlink]: https://github.com/remarkjs/remark-unlink | ||
[package-field]: doc/options.md#optionspackagefield | ||
[unified]: https://github.com/unifiedjs/unified | ||
[rc-path]: doc/options.md#optionsrcpath | ||
[unified-processor]: https://github.com/unifiedjs/unified#processor-1 | ||
[settings]: doc/options.md#optionssettings | ||
[unified-args]: https://github.com/unifiedjs/unified-args | ||
[detect-ignore]: doc/options.md#optionsdetectignore | ||
[unified-engine-gulp]: https://github.com/unifiedjs/unified-engine-gulp | ||
[ignore-name]: doc/options.md#optionsignorename | ||
[unified-language-server]: https://github.com/unifiedjs/unified-language-server | ||
[ignore-path]: doc/options.md#optionsignorepath | ||
[unified-settings]: https://github.com/unifiedjs/unified#settings | ||
[silently-ignore]: doc/options.md#optionssilentlyignore | ||
[unist-util-inspect]: https://github.com/syntax-tree/unist-util-inspect | ||
[plugin-prefix]: doc/options.md#optionspluginprefix | ||
[vfile]: https://github.com/vfile/vfile | ||
[config-transform]: doc/options.md#optionsconfigtransform | ||
[vfile-reporter]: https://github.com/vfile/vfile-reporter | ||
[plugins]: doc/options.md#optionsplugins | ||
[vfile-reporter-json]: https://github.com/vfile/vfile-reporter-json | ||
[reporter]: doc/options.md#optionsreporter | ||
[vfile-reporter-options]: https://github.com/vfile/vfile-reporter#options | ||
[reporteroptions]: doc/options.md#optionsreporteroptions | ||
[node-readable-stream]: https://nodejs.org/api/stream.html#readable-streams | ||
[color]: doc/options.md#optionscolor | ||
[node-writable-stream]: https://nodejs.org/api/stream.html#writable-streams | ||
[silent]: doc/options.md#optionssilent | ||
[config-files]: #config-files | ||
[quiet]: doc/options.md#optionsquiet | ||
[ignore-files]: #ignore-files | ||
[frail]: doc/options.md#optionsfrail | ||
[api-configuration]: #configuration | ||
[files]: doc/options.md#optionsfiles | ||
[api-engine]: #engineoptions-callback | ||
[configure]: doc/configure.md | ||
[api-completer]: #completer | ||
[ignore]: doc/ignore.md | ||
[api-callback]: #callback | ||
[plug-ins]: doc/plug-ins.md | ||
[api-config-transform]: #configtransform | ||
[atom]: https://github.com/unifiedjs/unified-engine-atom | ||
[api-context]: #context | ||
[gulp]: https://github.com/unifiedjs/unified-engine-gulp | ||
[api-file-set]: #fileset | ||
[args]: https://github.com/unifiedjs/unified-args | ||
[api-options]: #options | ||
[contributing]: https://github.com/unifiedjs/unified/blob/master/contributing.md | ||
[api-preset]: #preset | ||
[coc]: https://github.com/unifiedjs/unified/blob/master/code-of-conduct.md | ||
[api-resolve-from]: #resolvefrom | ||
[api-vfile-reporter]: #vfilereporter |
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
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
162368
206.37%49
96%4056
183.24%1673
470.99%1
-87.5%Yes
NaN21
10.53%15
50%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated