460 lines
16 KiB
JavaScript
460 lines
16 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.parseSync = exports.parse = exports.parseFromFilesSync = exports.parseFromFiles = exports.parseString = exports.parseBuffer = void 0;
|
|
const fs = __importStar(require("fs"));
|
|
const path = __importStar(require("path"));
|
|
const semver = __importStar(require("semver"));
|
|
const minimatch_1 = require("minimatch");
|
|
const wasm_1 = require("@one-ini/wasm");
|
|
// @ts-ignore So we can set the rootDir to be 'lib', without processing
|
|
// package.json
|
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
const escapedSep = new RegExp(path.sep.replace(/\\/g, '\\\\'), 'g');
|
|
const matchOptions = { matchBase: true, dot: true, noext: true };
|
|
// These are specified by the editorconfig script
|
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
const knownProps = {
|
|
end_of_line: true,
|
|
indent_style: true,
|
|
indent_size: true,
|
|
insert_final_newline: true,
|
|
trim_trailing_whitespace: true,
|
|
charset: true,
|
|
};
|
|
/**
|
|
* Parse a buffer using the faster one-ini WASM approach into something
|
|
* relatively easy to deal with in JS.
|
|
*
|
|
* @param data UTF8-encoded bytes.
|
|
* @returns Parsed contents. Will be truncated if there was a parse error.
|
|
*/
|
|
function parseBuffer(data) {
|
|
const parsed = (0, wasm_1.parse_to_uint32array)(data);
|
|
let cur = {};
|
|
const res = [[null, cur]];
|
|
let key = null;
|
|
for (let i = 0; i < parsed.length; i += 3) {
|
|
switch (parsed[i]) {
|
|
case wasm_1.TokenTypes.Section: {
|
|
cur = {};
|
|
res.push([
|
|
data.toString('utf8', parsed[i + 1], parsed[i + 2]),
|
|
cur,
|
|
]);
|
|
break;
|
|
}
|
|
case wasm_1.TokenTypes.Key:
|
|
key = data.toString('utf8', parsed[i + 1], parsed[i + 2]);
|
|
break;
|
|
case wasm_1.TokenTypes.Value: {
|
|
cur[key] = data.toString('utf8', parsed[i + 1], parsed[i + 2]);
|
|
break;
|
|
}
|
|
default: // Comments, etc.
|
|
break;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
exports.parseBuffer = parseBuffer;
|
|
/**
|
|
* Parses a string. If possible, you should always use ParseBuffer instead,
|
|
* since this function does a UTF16-to-UTF8 conversion first.
|
|
*
|
|
* @param data String to parse.
|
|
* @returns Parsed contents. Will be truncated if there was a parse error.
|
|
* @deprecated Use {@link ParseBuffer} instead.
|
|
*/
|
|
function parseString(data) {
|
|
return parseBuffer(Buffer.from(data));
|
|
}
|
|
exports.parseString = parseString;
|
|
/**
|
|
* Gets a list of *potential* filenames based on the path of the target
|
|
* filename.
|
|
*
|
|
* @param filepath File we are asking about.
|
|
* @param options Config file name and root directory
|
|
* @returns List of potential fully-qualified filenames that might have configs.
|
|
*/
|
|
function getConfigFileNames(filepath, options) {
|
|
const paths = [];
|
|
do {
|
|
filepath = path.dirname(filepath);
|
|
paths.push(path.join(filepath, options.config));
|
|
} while (filepath !== options.root);
|
|
return paths;
|
|
}
|
|
/**
|
|
* Take a combined config for the target file, and tweak it slightly based on
|
|
* which editorconfig version's rules we are using.
|
|
*
|
|
* @param matches Combined config.
|
|
* @param version Editorconfig version to enforce.
|
|
* @returns The passed-in matches object, modified in place.
|
|
*/
|
|
function processMatches(matches, version) {
|
|
// Set indent_size to 'tab' if indent_size is unspecified and
|
|
// indent_style is set to 'tab'.
|
|
if ('indent_style' in matches
|
|
&& matches.indent_style === 'tab'
|
|
&& !('indent_size' in matches)
|
|
&& semver.gte(version, '0.10.0')) {
|
|
matches.indent_size = 'tab';
|
|
}
|
|
// Set tab_width to indent_size if indent_size is specified and
|
|
// tab_width is unspecified
|
|
if ('indent_size' in matches
|
|
&& !('tab_width' in matches)
|
|
&& matches.indent_size !== 'tab') {
|
|
matches.tab_width = matches.indent_size;
|
|
}
|
|
// Set indent_size to tab_width if indent_size is 'tab'
|
|
if ('indent_size' in matches
|
|
&& 'tab_width' in matches
|
|
&& matches.indent_size === 'tab') {
|
|
matches.indent_size = matches.tab_width;
|
|
}
|
|
return matches;
|
|
}
|
|
function buildFullGlob(pathPrefix, glob) {
|
|
switch (glob.indexOf('/')) {
|
|
case -1:
|
|
glob = '**/' + glob;
|
|
break;
|
|
case 0:
|
|
glob = glob.substring(1);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// braces_escaped_backslash2
|
|
// backslash_not_on_windows
|
|
glob = glob.replace(/\\\\/g, '\\\\\\\\');
|
|
// star_star_over_separator{1,3,5,6,9,15}
|
|
glob = glob.replace(/\*\*/g, '{*,**/**/**}');
|
|
// NOT path.join. Must stay in forward slashes.
|
|
return new minimatch_1.Minimatch(`${pathPrefix}/${glob}`, matchOptions);
|
|
}
|
|
/**
|
|
* Normalize the properties read from a config file so that their key names
|
|
* are lowercased for the known properties, and their values are parsed into
|
|
* the correct JS types if possible.
|
|
*
|
|
* @param options
|
|
* @returns
|
|
*/
|
|
function normalizeProps(options) {
|
|
const props = {};
|
|
for (const key in options) {
|
|
if (options.hasOwnProperty(key)) {
|
|
const value = options[key];
|
|
const key2 = key.toLowerCase();
|
|
let value2 = value;
|
|
// @ts-ignore -- Fix types here
|
|
if (knownProps[key2]) {
|
|
// All of the values for the known props are lowercase.
|
|
value2 = String(value).toLowerCase();
|
|
}
|
|
try {
|
|
value2 = JSON.parse(String(value));
|
|
}
|
|
catch (e) { }
|
|
if (typeof value2 === 'undefined' || value2 === null) {
|
|
// null and undefined are values specific to JSON (no special meaning
|
|
// in editorconfig) & should just be returned as regular strings.
|
|
value2 = String(value);
|
|
}
|
|
// @ts-ignore -- Fix types here
|
|
props[key2] = value2;
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
/**
|
|
* Take the contents of a config file, and prepare it for use. If a cache is
|
|
* provided, the result will be stored there. As such, all of the higher-CPU
|
|
* work that is per-file should be done here.
|
|
*
|
|
* @param filepath The fully-qualified path of the file.
|
|
* @param contents The contents as read from that file.
|
|
* @param options Access to the cache.
|
|
* @returns Processed file with globs pre-computed.
|
|
*/
|
|
function processFileContents(filepath, contents, options) {
|
|
let res;
|
|
if (!contents) {
|
|
// Negative cache
|
|
res = {
|
|
root: false,
|
|
notfound: true,
|
|
name: filepath,
|
|
config: [[null, {}, null]],
|
|
};
|
|
}
|
|
else {
|
|
let pathPrefix = path.dirname(filepath);
|
|
if (path.sep !== '/') {
|
|
// Windows-only
|
|
pathPrefix = pathPrefix.replace(escapedSep, '/');
|
|
}
|
|
// After Windows path backslash's are turned into slashes, so that
|
|
// the backslashes we add here aren't turned into forward slashes:
|
|
// All of these characters are special to minimatch, but can be
|
|
// forced into path names on many file systems. Escape them. Note
|
|
// that these are in the order of the case statement in minimatch.
|
|
pathPrefix = pathPrefix.replace(/[?*+@!()|[\]{}]/g, '\\$&');
|
|
// I can't think of a way for this to happen in the filesystems I've
|
|
// seen (because of the path.dirname above), but let's be thorough.
|
|
pathPrefix = pathPrefix.replace(/^#/, '\\#');
|
|
const globbed = parseBuffer(contents).map(([name, body]) => [
|
|
name,
|
|
normalizeProps(body),
|
|
name ? buildFullGlob(pathPrefix, name) : null,
|
|
]);
|
|
res = {
|
|
root: !!globbed[0][1].root,
|
|
name: filepath,
|
|
config: globbed,
|
|
};
|
|
}
|
|
if (options.cache) {
|
|
options.cache.set(filepath, res);
|
|
}
|
|
return res;
|
|
}
|
|
/**
|
|
* Get a file from the cache, or read its contents from disk, process, and
|
|
* insert into the cache (if configured).
|
|
*
|
|
* @param filepath The fully-qualified path of the config file.
|
|
* @param options Access to the cache, if configured.
|
|
* @returns The processed file, or undefined if there was an error reading it.
|
|
*/
|
|
async function getConfig(filepath, options) {
|
|
if (options.cache) {
|
|
const cached = options.cache.get(filepath);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
}
|
|
const contents = await new Promise(resolve => {
|
|
fs.readFile(filepath, (_, buf) => {
|
|
// Ignore errors. contents will be undefined
|
|
// Perhaps only file-not-found should be ignored?
|
|
resolve(buf);
|
|
});
|
|
});
|
|
return processFileContents(filepath, contents, options);
|
|
}
|
|
/**
|
|
* Get a file from the cache, or read its contents from disk, process, and
|
|
* insert into the cache (if configured). Synchronous.
|
|
*
|
|
* @param filepath The fully-qualified path of the config file.
|
|
* @param options Access to the cache, if configured.
|
|
* @returns The processed file, or undefined if there was an error reading it.
|
|
*/
|
|
function getConfigSync(filepath, options) {
|
|
if (options.cache) {
|
|
const cached = options.cache.get(filepath);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
}
|
|
let contents;
|
|
try {
|
|
contents = fs.readFileSync(filepath);
|
|
}
|
|
catch (_) {
|
|
// Ignore errors
|
|
// Perhaps only file-not-found should be ignored
|
|
}
|
|
return processFileContents(filepath, contents, options);
|
|
}
|
|
/**
|
|
* Get all of the possibly-existing config files, stopping when one is marked
|
|
* root=true.
|
|
*
|
|
* @param files List of potential files
|
|
* @param options Access to cache if configured
|
|
* @returns List of processed configs for existing files
|
|
*/
|
|
async function getAllConfigs(files, options) {
|
|
const configs = [];
|
|
for (const file of files) {
|
|
const config = await getConfig(file, options);
|
|
if (!config.notfound) {
|
|
configs.push(config);
|
|
if (config.root) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return configs;
|
|
}
|
|
/**
|
|
* Get all of the possibly-existing config files, stopping when one is marked
|
|
* root=true. Synchronous.
|
|
*
|
|
* @param files List of potential files
|
|
* @param options Access to cache if configured
|
|
* @returns List of processed configs for existing files
|
|
*/
|
|
function getAllConfigsSync(files, options) {
|
|
const configs = [];
|
|
for (const file of files) {
|
|
const config = getConfigSync(file, options);
|
|
if (!config.notfound) {
|
|
configs.push(config);
|
|
if (config.root) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return configs;
|
|
}
|
|
/**
|
|
* Normalize the options passed in to the publicly-visible functions.
|
|
*
|
|
* @param filepath The name of the target file, relative to process.cwd().
|
|
* @param options Potentially-incomplete options.
|
|
* @returns The fully-qualified target file name and the normalized options.
|
|
*/
|
|
function opts(filepath, options = {}) {
|
|
const resolvedFilePath = path.resolve(filepath);
|
|
return [
|
|
resolvedFilePath,
|
|
{
|
|
config: options.config || '.editorconfig',
|
|
version: options.version || package_json_1.default.version,
|
|
root: path.resolve(options.root || path.parse(resolvedFilePath).root),
|
|
files: options.files,
|
|
cache: options.cache,
|
|
},
|
|
];
|
|
}
|
|
/**
|
|
* Low-level interface, which exists only for backward-compatibility.
|
|
* Deprecated.
|
|
*
|
|
* @param filepath The name of the target file, relative to process.cwd().
|
|
* @param files A promise for a list of objects describing the files.
|
|
* @param options All options
|
|
* @returns The properties found for filepath
|
|
* @deprecated
|
|
*/
|
|
async function parseFromFiles(filepath, files, options = {}) {
|
|
return parseFromFilesSync(filepath, await files, options);
|
|
}
|
|
exports.parseFromFiles = parseFromFiles;
|
|
/**
|
|
* Low-level interface, which exists only for backward-compatibility.
|
|
* Deprecated.
|
|
*
|
|
* @param filepath The name of the target file, relative to process.cwd().
|
|
* @param files A list of objects describing the files.
|
|
* @param options All options
|
|
* @returns The properties found for filepath
|
|
* @deprecated
|
|
*/
|
|
function parseFromFilesSync(filepath, files, options = {}) {
|
|
const [resolvedFilePath, processedOptions] = opts(filepath, options);
|
|
const configs = [];
|
|
for (const ecf of files) {
|
|
let cfg;
|
|
if (!options.cache || !(cfg = options.cache.get(ecf.name))) { // Single "="!
|
|
cfg = processFileContents(ecf.name, ecf.contents, processedOptions);
|
|
}
|
|
if (!cfg.notfound) {
|
|
configs.push(cfg);
|
|
}
|
|
if (cfg.root) {
|
|
break;
|
|
}
|
|
}
|
|
return combine(resolvedFilePath, configs, processedOptions);
|
|
}
|
|
exports.parseFromFilesSync = parseFromFilesSync;
|
|
/**
|
|
* Combine the pre-parsed results of all matching config file sections, in
|
|
* order.
|
|
*
|
|
* @param filepath The target file path
|
|
* @param configs All of the found config files, up to the root
|
|
* @param options Adds to `options.files` if it exists
|
|
* @returns Combined properties
|
|
*/
|
|
function combine(filepath, configs, options) {
|
|
const ret = configs.reverse().reduce((props, processed) => {
|
|
for (const [name, body, glob] of processed.config) {
|
|
if (glob && glob.match(filepath)) {
|
|
Object.assign(props, body);
|
|
if (options.files) {
|
|
options.files.push({
|
|
fileName: processed.name,
|
|
glob: name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return props;
|
|
}, {});
|
|
return processMatches(ret, options.version);
|
|
}
|
|
/**
|
|
* Find all of the properties from matching sections in config files in the
|
|
* same directory or toward the root of the filesystem.
|
|
*
|
|
* @param filepath The target file name, relative to process.cwd().
|
|
* @param options All options
|
|
* @returns Combined properties for the target file
|
|
*/
|
|
async function parse(filepath, options = {}) {
|
|
const [resolvedFilePath, processedOptions] = opts(filepath, options);
|
|
const filepaths = getConfigFileNames(resolvedFilePath, processedOptions);
|
|
const configs = await getAllConfigs(filepaths, processedOptions);
|
|
return combine(resolvedFilePath, configs, processedOptions);
|
|
}
|
|
exports.parse = parse;
|
|
/**
|
|
* Find all of the properties from matching sections in config files in the
|
|
* same directory or toward the root of the filesystem. Synchronous.
|
|
*
|
|
* @param filepath The target file name, relative to process.cwd().
|
|
* @param options All options
|
|
* @returns Combined properties for the target file
|
|
*/
|
|
function parseSync(filepath, options = {}) {
|
|
const [resolvedFilePath, processedOptions] = opts(filepath, options);
|
|
const filepaths = getConfigFileNames(resolvedFilePath, processedOptions);
|
|
const configs = getAllConfigsSync(filepaths, processedOptions);
|
|
return combine(resolvedFilePath, configs, processedOptions);
|
|
}
|
|
exports.parseSync = parseSync;
|