mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
628 lines
14 KiB
628 lines
14 KiB
/**
|
|
* @author Titus Wormer
|
|
* @copyright 2015 Titus Wormer
|
|
* @license MIT
|
|
* @module vfile
|
|
* @fileoverview Virtual file format to attach additional
|
|
* information related to processed input. Similar to
|
|
* `wearefractal/vinyl`. Additionally, `VFile` can be
|
|
* passed directly to ESLint formatters to visualise
|
|
* warnings and errors relating to a file.
|
|
* @example
|
|
* var VFile = require('vfile');
|
|
*
|
|
* var file = new VFile({
|
|
* 'directory': '~',
|
|
* 'filename': 'example',
|
|
* 'extension': 'txt',
|
|
* 'contents': 'Foo *bar* baz'
|
|
* });
|
|
*
|
|
* file.toString(); // 'Foo *bar* baz'
|
|
* file.filePath(); // '~/example.txt'
|
|
*
|
|
* file.move({'extension': 'md'});
|
|
* file.filePath(); // '~/example.md'
|
|
*
|
|
* file.warn('Something went wrong', {'line': 2, 'column': 3});
|
|
* // { [~/example.md:2:3: Something went wrong]
|
|
* // name: '~/example.md:2:3',
|
|
* // file: '~/example.md',
|
|
* // reason: 'Something went wrong',
|
|
* // line: 2,
|
|
* // column: 3,
|
|
* // fatal: false }
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/* eslint-env commonjs */
|
|
|
|
var proto;
|
|
|
|
var SEPARATOR = '/';
|
|
|
|
try {
|
|
SEPARATOR = require('pa' + 'th').sep;
|
|
} catch (e) { /* empty */ }
|
|
|
|
/**
|
|
* Construct a new file message.
|
|
*
|
|
* Note: We cannot invoke `Error` on the created context,
|
|
* as that adds readonly `line` and `column` attributes on
|
|
* Safari 9, thus throwing and failing the data.
|
|
*
|
|
* @example
|
|
* var message = new VFileMessage('Whoops!');
|
|
*
|
|
* message instanceof Error // true
|
|
*
|
|
* @constructor
|
|
* @class {VFileMessage}
|
|
* @param {string} reason - Reason for messaging.
|
|
* @property {boolean} [fatal=null] - Whether the message
|
|
* is fatal.
|
|
* @property {string} [name=''] - File-name and positional
|
|
* information.
|
|
* @property {string} [file=''] - File-path.
|
|
* @property {string} [reason=''] - Reason for messaging.
|
|
* @property {number} [line=null] - Start of message.
|
|
* @property {number} [column=null] - Start of message.
|
|
* @property {Position|Location} [location=null] - Place of
|
|
* message.
|
|
* @property {string} [stack] - Stack-trace of warning.
|
|
*/
|
|
function VFileMessage(reason) {
|
|
this.message = reason;
|
|
}
|
|
|
|
/**
|
|
* Inherit from `Error#`.
|
|
*/
|
|
function VFileMessagePrototype() {}
|
|
|
|
VFileMessagePrototype.prototype = Error.prototype;
|
|
|
|
proto = new VFileMessagePrototype();
|
|
|
|
VFileMessage.prototype = proto;
|
|
|
|
/*
|
|
* Expose defaults.
|
|
*/
|
|
|
|
proto.file = proto.name = proto.reason = proto.message = proto.stack = '';
|
|
proto.fatal = proto.column = proto.line = null;
|
|
|
|
/**
|
|
* File-related message with location information.
|
|
*
|
|
* @typedef {Error} VFileMessage
|
|
* @property {string} name - (Starting) location of the
|
|
* message, preceded by its file-path when available,
|
|
* and joined by `:`. Used internally by the native
|
|
* `Error#toString()`.
|
|
* @property {string} file - File-path.
|
|
* @property {string} reason - Reason for message.
|
|
* @property {number?} line - Line of message, when
|
|
* available.
|
|
* @property {number?} column - Column of message, when
|
|
* available.
|
|
* @property {string?} stack - Stack of message, when
|
|
* available.
|
|
* @property {boolean?} fatal - Whether the associated file
|
|
* is still processable.
|
|
*/
|
|
|
|
/**
|
|
* Stringify a position.
|
|
*
|
|
* @example
|
|
* stringify({'line': 1, 'column': 3}) // '1:3'
|
|
* stringify({'line': 1}) // '1:1'
|
|
* stringify({'column': 3}) // '1:3'
|
|
* stringify() // '1:1'
|
|
*
|
|
* @private
|
|
* @param {Object?} [position] - Single position, like
|
|
* those available at `node.position.start`.
|
|
* @return {string} - Compiled location.
|
|
*/
|
|
function stringify(position) {
|
|
if (!position) {
|
|
position = {};
|
|
}
|
|
|
|
return (position.line || 1) + ':' + (position.column || 1);
|
|
}
|
|
|
|
/**
|
|
* ESLint's formatter API expects `filePath` to be a
|
|
* string. This hack supports invocation as well as
|
|
* implicit coercion.
|
|
*
|
|
* @example
|
|
* var file = new VFile({
|
|
* 'filename': 'example',
|
|
* 'extension': 'txt'
|
|
* });
|
|
*
|
|
* filePath = filePathFactory(file);
|
|
*
|
|
* String(filePath); // 'example.txt'
|
|
* filePath(); // 'example.txt'
|
|
*
|
|
* @private
|
|
* @param {VFile} file - Virtual file.
|
|
* @return {Function} - `filePath` getter.
|
|
*/
|
|
function filePathFactory(file) {
|
|
/**
|
|
* Get the filename, with extension and directory, if applicable.
|
|
*
|
|
* @example
|
|
* var file = new VFile({
|
|
* 'directory': '~',
|
|
* 'filename': 'example',
|
|
* 'extension': 'txt'
|
|
* });
|
|
*
|
|
* String(file.filePath); // ~/example.txt
|
|
* file.filePath() // ~/example.txt
|
|
*
|
|
* @memberof {VFile}
|
|
* @property {Function} toString - Itself. ESLint's
|
|
* formatter API expects `filePath` to be `string`.
|
|
* This hack supports invocation as well as implicit
|
|
* coercion.
|
|
* @return {string} - If the `vFile` has a `filename`,
|
|
* it will be prefixed with the directory (slashed),
|
|
* if applicable, and suffixed with the (dotted)
|
|
* extension (if applicable). Otherwise, an empty
|
|
* string is returned.
|
|
*/
|
|
function filePath() {
|
|
var directory = file.directory;
|
|
var separator;
|
|
|
|
if (file.filename || file.extension) {
|
|
separator = directory.charAt(directory.length - 1);
|
|
|
|
if (separator === '/' || separator === '\\') {
|
|
directory = directory.slice(0, -1);
|
|
}
|
|
|
|
if (directory === '.') {
|
|
directory = '';
|
|
}
|
|
|
|
return (directory ? directory + SEPARATOR : '') +
|
|
file.filename +
|
|
(file.extension ? '.' + file.extension : '');
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
filePath.toString = filePath;
|
|
|
|
return filePath;
|
|
}
|
|
|
|
/**
|
|
* Get the filename with extantion.
|
|
*
|
|
* @example
|
|
* var file = new VFile({
|
|
* 'directory': '~/foo/bar'
|
|
* 'filename': 'example',
|
|
* 'extension': 'txt'
|
|
* });
|
|
*
|
|
* file.basename() // example.txt
|
|
*
|
|
* @memberof {VFile}
|
|
* @return {string} - name of file with extantion.
|
|
*/
|
|
function basename() {
|
|
var self = this;
|
|
var extension = self.extension;
|
|
|
|
if (self.filename || extension) {
|
|
return self.filename + (extension ? '.' + extension : '');
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Construct a new file.
|
|
*
|
|
* @example
|
|
* var file = new VFile({
|
|
* 'directory': '~',
|
|
* 'filename': 'example',
|
|
* 'extension': 'txt',
|
|
* 'contents': 'Foo *bar* baz'
|
|
* });
|
|
*
|
|
* file === VFile(file) // true
|
|
* file === new VFile(file) // true
|
|
* VFile('foo') instanceof VFile // true
|
|
*
|
|
* @constructor
|
|
* @class {VFile}
|
|
* @param {Object|VFile|string} [options] - either an
|
|
* options object, or the value of `contents` (both
|
|
* optional). When a `file` is passed in, it's
|
|
* immediately returned.
|
|
* @property {string} [contents=''] - Content of file.
|
|
* @property {string} [directory=''] - Path to parent
|
|
* directory.
|
|
* @property {string} [filename=''] - Filename.
|
|
* A file-path can still be generated when no filename
|
|
* exists.
|
|
* @property {string} [extension=''] - Extension.
|
|
* A file-path can still be generated when no extension
|
|
* exists.
|
|
* @property {boolean?} quiet - Whether an error created by
|
|
* `VFile#fail()` is returned (when truthy) or thrown
|
|
* (when falsey). Ensure all `messages` associated with
|
|
* a file are handled properly when setting this to
|
|
* `true`.
|
|
* @property {Array.<VFileMessage>} messages - List of associated
|
|
* messages.
|
|
*/
|
|
function VFile(options) {
|
|
var self = this;
|
|
|
|
/*
|
|
* No `new` operator.
|
|
*/
|
|
|
|
if (!(self instanceof VFile)) {
|
|
return new VFile(options);
|
|
}
|
|
|
|
/*
|
|
* Given file.
|
|
*/
|
|
|
|
if (
|
|
options &&
|
|
typeof options.message === 'function' &&
|
|
typeof options.hasFailed === 'function'
|
|
) {
|
|
return options;
|
|
}
|
|
|
|
if (!options) {
|
|
options = {};
|
|
} else if (typeof options === 'string') {
|
|
options = {
|
|
'contents': options
|
|
};
|
|
}
|
|
|
|
self.contents = options.contents || '';
|
|
|
|
self.messages = [];
|
|
|
|
/*
|
|
* Make sure eslint’s formatters stringify `filePath`
|
|
* properly.
|
|
*/
|
|
|
|
self.filePath = filePathFactory(self);
|
|
|
|
self.history = [];
|
|
|
|
self.move({
|
|
'filename': options.filename,
|
|
'directory': options.directory,
|
|
'extension': options.extension
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the value of the file.
|
|
*
|
|
* @example
|
|
* var vFile = new VFile('Foo');
|
|
* String(vFile); // 'Foo'
|
|
*
|
|
* @this {VFile}
|
|
* @memberof {VFile}
|
|
* @return {string} - value at the `contents` property
|
|
* in context.
|
|
*/
|
|
function toString() {
|
|
return this.contents;
|
|
}
|
|
|
|
/**
|
|
* Move a file by passing a new directory, filename,
|
|
* and extension. When these are not given, the default
|
|
* values are kept.
|
|
*
|
|
* @example
|
|
* var file = new VFile({
|
|
* 'directory': '~',
|
|
* 'filename': 'example',
|
|
* 'extension': 'txt',
|
|
* 'contents': 'Foo *bar* baz'
|
|
* });
|
|
*
|
|
* file.move({'directory': '/var/www'});
|
|
* file.filePath(); // '/var/www/example.txt'
|
|
*
|
|
* file.move({'extension': 'md'});
|
|
* file.filePath(); // '/var/www/example.md'
|
|
*
|
|
* @this {VFile}
|
|
* @memberof {VFile}
|
|
* @param {Object?} [options] - Configuration.
|
|
* @return {VFile} - Context object.
|
|
*/
|
|
function move(options) {
|
|
var self = this;
|
|
var before = self.filePath();
|
|
var after;
|
|
|
|
if (!options) {
|
|
options = {};
|
|
}
|
|
|
|
self.directory = options.directory || self.directory || '';
|
|
self.filename = options.filename || self.filename || '';
|
|
self.extension = options.extension || self.extension || '';
|
|
|
|
after = self.filePath();
|
|
|
|
if (after && before !== after) {
|
|
self.history.push(after);
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
/**
|
|
* Create a message with `reason` at `position`.
|
|
* When an error is passed in as `reason`, copies the
|
|
* stack. This does not add a message to `messages`.
|
|
*
|
|
* @example
|
|
* var file = new VFile();
|
|
*
|
|
* file.message('Something went wrong');
|
|
* // { [1:1: Something went wrong]
|
|
* // name: '1:1',
|
|
* // file: '',
|
|
* // reason: 'Something went wrong',
|
|
* // line: null,
|
|
* // column: null }
|
|
*
|
|
* @this {VFile}
|
|
* @memberof {VFile}
|
|
* @param {string|Error} reason - Reason for message.
|
|
* @param {Node|Location|Position} [position] - Location
|
|
* of message in file.
|
|
* @param {string} [ruleId] - Category of warning.
|
|
* @return {VFileMessage} - File-related message with
|
|
* location information.
|
|
*/
|
|
function message(reason, position, ruleId) {
|
|
var filePath = this.filePath();
|
|
var range;
|
|
var err;
|
|
var location = {
|
|
'start': {
|
|
'line': null,
|
|
'column': null
|
|
},
|
|
'end': {
|
|
'line': null,
|
|
'column': null
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Node / location / position.
|
|
*/
|
|
|
|
if (position && position.position) {
|
|
position = position.position;
|
|
}
|
|
|
|
if (position && position.start) {
|
|
range = stringify(position.start) + '-' + stringify(position.end);
|
|
location = position;
|
|
position = position.start;
|
|
} else {
|
|
range = stringify(position);
|
|
|
|
if (position) {
|
|
location.start = position;
|
|
location.end.line = null;
|
|
location.end.column = null;
|
|
}
|
|
}
|
|
|
|
err = new VFileMessage(reason.message || reason);
|
|
|
|
err.name = (filePath ? filePath + ':' : '') + range;
|
|
err.file = filePath;
|
|
err.reason = reason.message || reason;
|
|
err.line = position ? position.line : null;
|
|
err.column = position ? position.column : null;
|
|
err.location = location;
|
|
err.ruleId = ruleId || null;
|
|
|
|
if (reason.stack) {
|
|
err.stack = reason.stack;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* Warn. Creates a non-fatal message (see `VFile#message()`),
|
|
* and adds it to the file's `messages` list.
|
|
*
|
|
* @example
|
|
* var file = new VFile();
|
|
*
|
|
* file.warn('Something went wrong');
|
|
* // { [1:1: Something went wrong]
|
|
* // name: '1:1',
|
|
* // file: '',
|
|
* // reason: 'Something went wrong',
|
|
* // line: null,
|
|
* // column: null,
|
|
* // fatal: false }
|
|
*
|
|
* @see VFile#message
|
|
* @this {VFile}
|
|
* @memberof {VFile}
|
|
*/
|
|
function warn() {
|
|
var err = this.message.apply(this, arguments);
|
|
|
|
err.fatal = false;
|
|
|
|
this.messages.push(err);
|
|
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* Fail. Creates a fatal message (see `VFile#message()`),
|
|
* sets `fatal: true`, adds it to the file's
|
|
* `messages` list.
|
|
*
|
|
* If `quiet` is not `true`, throws the error.
|
|
*
|
|
* @example
|
|
* var file = new VFile();
|
|
*
|
|
* file.fail('Something went wrong');
|
|
* // 1:1: Something went wrong
|
|
* // at VFile.exception (vfile/index.js:296:11)
|
|
* // at VFile.fail (vfile/index.js:360:20)
|
|
* // at repl:1:6
|
|
*
|
|
* file.quiet = true;
|
|
* file.fail('Something went wrong');
|
|
* // { [1:1: Something went wrong]
|
|
* // name: '1:1',
|
|
* // file: '',
|
|
* // reason: 'Something went wrong',
|
|
* // line: null,
|
|
* // column: null,
|
|
* // fatal: true }
|
|
*
|
|
* @this {VFile}
|
|
* @memberof {VFile}
|
|
* @throws {VFileMessage} - When not `quiet: true`.
|
|
* @param {string|Error} reason - Reason for failure.
|
|
* @param {Node|Location|Position} [position] - Place
|
|
* of failure in file.
|
|
* @return {VFileMessage} - Unless thrown, of course.
|
|
*/
|
|
function fail(reason, position) {
|
|
var err = this.message(reason, position);
|
|
|
|
err.fatal = true;
|
|
|
|
this.messages.push(err);
|
|
|
|
if (!this.quiet) {
|
|
throw err;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* Check if a fatal message occurred making the file no
|
|
* longer processable.
|
|
*
|
|
* @example
|
|
* var file = new VFile();
|
|
* file.quiet = true;
|
|
*
|
|
* file.hasFailed(); // false
|
|
*
|
|
* file.fail('Something went wrong');
|
|
* file.hasFailed(); // true
|
|
*
|
|
* @this {VFile}
|
|
* @memberof {VFile}
|
|
* @return {boolean} - `true` if at least one of file's
|
|
* `messages` has a `fatal` property set to `true`
|
|
*/
|
|
function hasFailed() {
|
|
var messages = this.messages;
|
|
var index = -1;
|
|
var length = messages.length;
|
|
|
|
while (++index < length) {
|
|
if (messages[index].fatal) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Access metadata.
|
|
*
|
|
* @example
|
|
* var file = new VFile('Foo');
|
|
*
|
|
* file.namespace('foo').bar = 'baz';
|
|
*
|
|
* console.log(file.namespace('foo').bar) // 'baz';
|
|
*
|
|
* @this {VFile}
|
|
* @memberof {VFile}
|
|
* @param {string} key - Namespace key.
|
|
* @return {Object} - Private space.
|
|
*/
|
|
function namespace(key) {
|
|
var self = this;
|
|
var space = self.data;
|
|
|
|
if (!space) {
|
|
space = self.data = {};
|
|
}
|
|
|
|
if (!space[key]) {
|
|
space[key] = {};
|
|
}
|
|
|
|
return space[key];
|
|
}
|
|
|
|
/*
|
|
* Methods.
|
|
*/
|
|
|
|
proto = VFile.prototype;
|
|
|
|
proto.basename = basename;
|
|
proto.move = move;
|
|
proto.toString = toString;
|
|
proto.message = message;
|
|
proto.warn = warn;
|
|
proto.fail = fail;
|
|
proto.hasFailed = hasFailed;
|
|
proto.namespace = namespace;
|
|
|
|
/*
|
|
* Expose.
|
|
*/
|
|
|
|
module.exports = VFile;
|
|
|