@jsenv/prettier-check-project
Version:
Format staged or project files with prettier.
1,901 lines (1,593 loc) • 56.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var module$1 = require('module');
var url$1 = require('url');
var fs = require('fs');
require('crypto');
var path = require('path');
var util = require('util');
var child_process = require('child_process');
/* global require, __filename */
const nodeRequire = require;
const filenameContainsBackSlashes = __filename.indexOf("\\") > -1;
const url = filenameContainsBackSlashes ? `file:///${__filename.replace(/\\/g, "/")}` : `file://${__filename}`;
const assertUrlLike = (value, name = "url") => {
if (typeof value !== "string") {
throw new TypeError(`${name} must be a url string, got ${value}`);
}
if (isWindowsPathnameSpecifier(value)) {
throw new TypeError(`${name} must be a url but looks like a windows pathname, got ${value}`);
}
if (!hasScheme(value)) {
throw new TypeError(`${name} must be a url and no scheme found, got ${value}`);
}
};
const isWindowsPathnameSpecifier = specifier => {
const firstChar = specifier[0];
if (!/[a-zA-Z]/.test(firstChar)) return false;
const secondChar = specifier[1];
if (secondChar !== ":") return false;
const thirdChar = specifier[2];
return thirdChar === "/" || thirdChar === "\\";
};
const hasScheme = specifier => /^[a-zA-Z]+:/.test(specifier);
// https://git-scm.com/docs/gitignore
const applySpecifierPatternMatching = ({
specifier,
url,
...rest
} = {}) => {
assertUrlLike(specifier, "specifier");
assertUrlLike(url, "url");
if (Object.keys(rest).length) {
throw new Error(`received more parameters than expected.
--- name of unexpected parameters ---
${Object.keys(rest)}
--- name of expected parameters ---
specifier, url`);
}
return applyPatternMatching(specifier, url);
};
const applyPatternMatching = (pattern, string) => {
let patternIndex = 0;
let index = 0;
let remainingPattern = pattern;
let remainingString = string; // eslint-disable-next-line no-constant-condition
while (true) {
// pattern consumed and string consumed
if (remainingPattern === "" && remainingString === "") {
// pass because string fully matched pattern
return pass({
patternIndex,
index
});
} // pattern consumed, string not consumed
if (remainingPattern === "" && remainingString !== "") {
// fails because string longer than expected
return fail({
patternIndex,
index
});
} // from this point pattern is not consumed
// string consumed, pattern not consumed
if (remainingString === "") {
// pass because trailing "**" is optional
if (remainingPattern === "**") {
return pass({
patternIndex: patternIndex + 2,
index
});
} // fail because string shorted than expected
return fail({
patternIndex,
index
});
} // from this point pattern and string are not consumed
// fast path trailing slash
if (remainingPattern === "/") {
// pass because trailing slash matches remaining
if (remainingString[0] === "/") {
return pass({
patternIndex: patternIndex + 1,
index: string.length
});
}
return fail({
patternIndex,
index
});
} // fast path trailing '**'
if (remainingPattern === "**") {
// pass because trailing ** matches remaining
return pass({
patternIndex: patternIndex + 2,
index: string.length
});
} // pattern leading **
if (remainingPattern.slice(0, 2) === "**") {
// consumes "**"
remainingPattern = remainingPattern.slice(2);
patternIndex += 2;
if (remainingPattern[0] === "/") {
// consumes "/"
remainingPattern = remainingPattern.slice(1);
patternIndex += 1;
} // pattern ending with ** always match remaining string
if (remainingPattern === "") {
return pass({
patternIndex,
index: string.length
});
}
const skipResult = skipUntilMatch({
pattern: remainingPattern,
string: remainingString
});
if (!skipResult.matched) {
return fail({
patternIndex: patternIndex + skipResult.patternIndex,
index: index + skipResult.index
});
}
return pass({
patternIndex: pattern.length,
index: string.length
});
}
if (remainingPattern[0] === "*") {
// consumes "*"
remainingPattern = remainingPattern.slice(1);
patternIndex += 1; // la c'est plus délicat, il faut que remainingString
// ne soit composé que de truc !== '/'
if (remainingPattern === "") {
const slashIndex = remainingString.indexOf("/");
if (slashIndex > -1) {
return fail({
patternIndex,
index: index + slashIndex
});
}
return pass({
patternIndex,
index: string.length
});
} // the next char must not the one expected by remainingPattern[0]
// because * is greedy and expect to skip one char
if (remainingPattern[0] === remainingString[0]) {
return fail({
patternIndex: patternIndex - "*".length,
index
});
}
const skipResult = skipUntilMatch({
pattern: remainingPattern,
string: remainingString,
skippablePredicate: remainingString => remainingString[0] !== "/"
});
if (!skipResult.matched) {
return fail({
patternIndex: patternIndex + skipResult.patternIndex,
index: index + skipResult.index
});
}
return pass({
patternIndex: pattern.length,
index: string.length
});
}
if (remainingPattern[0] !== remainingString[0]) {
return fail({
patternIndex,
index
});
} // consumes next char
remainingPattern = remainingPattern.slice(1);
remainingString = remainingString.slice(1);
patternIndex += 1;
index += 1;
continue;
}
};
const skipUntilMatch = ({
pattern,
string,
skippablePredicate = () => true
}) => {
let index = 0;
let remainingString = string;
let bestMatch = null; // eslint-disable-next-line no-constant-condition
while (true) {
const matchAttempt = applyPatternMatching(pattern, remainingString);
if (matchAttempt.matched) {
bestMatch = matchAttempt;
break;
}
const skippable = skippablePredicate(remainingString);
bestMatch = fail({
patternIndex: bestMatch ? Math.max(bestMatch.patternIndex, matchAttempt.patternIndex) : matchAttempt.patternIndex,
index: index + matchAttempt.index
});
if (!skippable) {
break;
} // search against the next unattempted string
remainingString = remainingString.slice(matchAttempt.index + 1);
index += matchAttempt.index + 1;
if (remainingString === "") {
bestMatch = { ...bestMatch,
index: string.length
};
break;
}
continue;
}
return bestMatch;
};
const pass = ({
patternIndex,
index
}) => {
return {
matched: true,
index,
patternIndex
};
};
const fail = ({
patternIndex,
index
}) => {
return {
matched: false,
index,
patternIndex
};
};
const isPlainObject = value => {
if (value === null) {
return false;
}
if (typeof value === "object") {
if (Array.isArray(value)) {
return false;
}
return true;
}
return false;
};
const metaMapToSpecifierMetaMap = (metaMap, ...rest) => {
if (!isPlainObject(metaMap)) {
throw new TypeError(`metaMap must be a plain object, got ${metaMap}`);
}
if (rest.length) {
throw new Error(`received more arguments than expected.
--- number of arguments received ---
${1 + rest.length}
--- number of arguments expected ---
1`);
}
const specifierMetaMap = {};
Object.keys(metaMap).forEach(metaKey => {
const specifierValueMap = metaMap[metaKey];
if (!isPlainObject(specifierValueMap)) {
throw new TypeError(`metaMap value must be plain object, got ${specifierValueMap} for ${metaKey}`);
}
Object.keys(specifierValueMap).forEach(specifier => {
const metaValue = specifierValueMap[specifier];
const meta = {
[metaKey]: metaValue
};
specifierMetaMap[specifier] = specifier in specifierMetaMap ? { ...specifierMetaMap[specifier],
...meta
} : meta;
});
});
return specifierMetaMap;
};
const assertSpecifierMetaMap = (value, checkComposition = true) => {
if (!isPlainObject(value)) {
throw new TypeError(`specifierMetaMap must be a plain object, got ${value}`);
}
if (checkComposition) {
const plainObject = value;
Object.keys(plainObject).forEach(key => {
assertUrlLike(key, "specifierMetaMap key");
const value = plainObject[key];
if (value !== null && !isPlainObject(value)) {
throw new TypeError(`specifierMetaMap value must be a plain object or null, got ${value} under key ${key}`);
}
});
}
};
const normalizeSpecifierMetaMap = (specifierMetaMap, url, ...rest) => {
assertSpecifierMetaMap(specifierMetaMap, false);
assertUrlLike(url, "url");
if (rest.length) {
throw new Error(`received more arguments than expected.
--- number of arguments received ---
${2 + rest.length}
--- number of arguments expected ---
2`);
}
const specifierMetaMapNormalized = {};
Object.keys(specifierMetaMap).forEach(specifier => {
const specifierResolved = String(new URL(specifier, url));
specifierMetaMapNormalized[specifierResolved] = specifierMetaMap[specifier];
});
return specifierMetaMapNormalized;
};
const urlCanContainsMetaMatching = ({
url,
specifierMetaMap,
predicate,
...rest
}) => {
assertUrlLike(url, "url"); // the function was meants to be used on url ending with '/'
if (!url.endsWith("/")) {
throw new Error(`url should end with /, got ${url}`);
}
assertSpecifierMetaMap(specifierMetaMap);
if (typeof predicate !== "function") {
throw new TypeError(`predicate must be a function, got ${predicate}`);
}
if (Object.keys(rest).length) {
throw new Error(`received more parameters than expected.
--- name of unexpected parameters ---
${Object.keys(rest)}
--- name of expected parameters ---
url, specifierMetaMap, predicate`);
} // for full match we must create an object to allow pattern to override previous ones
let fullMatchMeta = {};
let someFullMatch = false; // for partial match, any meta satisfying predicate will be valid because
// we don't know for sure if pattern will still match for a file inside pathname
const partialMatchMetaArray = [];
Object.keys(specifierMetaMap).forEach(specifier => {
const meta = specifierMetaMap[specifier];
const {
matched,
index
} = applySpecifierPatternMatching({
specifier,
url
});
if (matched) {
someFullMatch = true;
fullMatchMeta = { ...fullMatchMeta,
...meta
};
} else if (someFullMatch === false && index >= url.length) {
partialMatchMetaArray.push(meta);
}
});
if (someFullMatch) {
return Boolean(predicate(fullMatchMeta));
}
return partialMatchMetaArray.some(partialMatchMeta => predicate(partialMatchMeta));
};
const urlToMeta = ({
url,
specifierMetaMap,
...rest
} = {}) => {
assertUrlLike(url);
assertSpecifierMetaMap(specifierMetaMap);
if (Object.keys(rest).length) {
throw new Error(`received more parameters than expected.
--- name of unexpected parameters ---
${Object.keys(rest)}
--- name of expected parameters ---
url, specifierMetaMap`);
}
return Object.keys(specifierMetaMap).reduce((previousMeta, specifier) => {
const {
matched
} = applySpecifierPatternMatching({
specifier,
url
});
if (matched) {
return { ...previousMeta,
...specifierMetaMap[specifier]
};
}
return previousMeta;
}, {});
};
const ensureUrlTrailingSlash = url => {
return url.endsWith("/") ? url : `${url}/`;
};
const isFileSystemPath = value => {
if (typeof value !== "string") {
throw new TypeError(`isFileSystemPath first arg must be a string, got ${value}`);
}
if (value[0] === "/") return true;
return startsWithWindowsDriveLetter(value);
};
const startsWithWindowsDriveLetter = string => {
const firstChar = string[0];
if (!/[a-zA-Z]/.test(firstChar)) return false;
const secondChar = string[1];
if (secondChar !== ":") return false;
return true;
};
const fileSystemPathToUrl = value => {
if (!isFileSystemPath(value)) {
throw new Error(`received an invalid value for fileSystemPath: ${value}`);
}
return String(url$1.pathToFileURL(value));
};
const assertAndNormalizeDirectoryUrl = value => {
let urlString;
if (value instanceof URL) {
urlString = value.href;
} else if (typeof value === "string") {
if (isFileSystemPath(value)) {
urlString = fileSystemPathToUrl(value);
} else {
try {
urlString = String(new URL(value));
} catch (e) {
throw new TypeError(`directoryUrl must be a valid url, received ${value}`);
}
}
} else {
throw new TypeError(`directoryUrl must be a string or an url, received ${value}`);
}
if (!urlString.startsWith("file://")) {
throw new Error(`directoryUrl must starts with file://, received ${value}`);
}
return ensureUrlTrailingSlash(urlString);
};
const assertAndNormalizeFileUrl = (value, baseUrl) => {
let urlString;
if (value instanceof URL) {
urlString = value.href;
} else if (typeof value === "string") {
if (isFileSystemPath(value)) {
urlString = fileSystemPathToUrl(value);
} else {
try {
urlString = String(new URL(value, baseUrl));
} catch (e) {
throw new TypeError(`fileUrl must be a valid url, received ${value}`);
}
}
} else {
throw new TypeError(`fileUrl must be a string or an url, received ${value}`);
}
if (!urlString.startsWith("file://")) {
throw new Error(`fileUrl must starts with file://, received ${value}`);
}
return urlString;
};
const statsToType = stats => {
if (stats.isFile()) return "file";
if (stats.isDirectory()) return "directory";
if (stats.isSymbolicLink()) return "symbolic-link";
if (stats.isFIFO()) return "fifo";
if (stats.isSocket()) return "socket";
if (stats.isCharacterDevice()) return "character-device";
if (stats.isBlockDevice()) return "block-device";
return undefined;
};
const urlToFileSystemPath = fileUrl => {
if (fileUrl[fileUrl.length - 1] === "/") {
// remove trailing / so that nodejs path becomes predictable otherwise it logs
// the trailing slash on linux but does not on windows
fileUrl = fileUrl.slice(0, -1);
}
const fileSystemPath = url$1.fileURLToPath(fileUrl);
return fileSystemPath;
};
// https://github.com/coderaiser/cloudcmd/issues/63#issuecomment-195478143
// https://nodejs.org/api/fs.html#fs_file_modes
// https://github.com/TooTallNate/stat-mode
// cannot get from fs.constants because they are not available on windows
const S_IRUSR = 256;
/* 0000400 read permission, owner */
const S_IWUSR = 128;
/* 0000200 write permission, owner */
const S_IXUSR = 64;
/* 0000100 execute/search permission, owner */
const S_IRGRP = 32;
/* 0000040 read permission, group */
const S_IWGRP = 16;
/* 0000020 write permission, group */
const S_IXGRP = 8;
/* 0000010 execute/search permission, group */
const S_IROTH = 4;
/* 0000004 read permission, others */
const S_IWOTH = 2;
/* 0000002 write permission, others */
const S_IXOTH = 1;
const permissionsToBinaryFlags = ({
owner,
group,
others
}) => {
let binaryFlags = 0;
if (owner.read) binaryFlags |= S_IRUSR;
if (owner.write) binaryFlags |= S_IWUSR;
if (owner.execute) binaryFlags |= S_IXUSR;
if (group.read) binaryFlags |= S_IRGRP;
if (group.write) binaryFlags |= S_IWGRP;
if (group.execute) binaryFlags |= S_IXGRP;
if (others.read) binaryFlags |= S_IROTH;
if (others.write) binaryFlags |= S_IWOTH;
if (others.execute) binaryFlags |= S_IXOTH;
return binaryFlags;
};
const writeFileSystemNodePermissions = async (source, permissions) => {
const sourceUrl = assertAndNormalizeFileUrl(source);
const sourcePath = urlToFileSystemPath(sourceUrl);
let binaryFlags;
if (typeof permissions === "object") {
permissions = {
owner: {
read: getPermissionOrComputeDefault("read", "owner", permissions),
write: getPermissionOrComputeDefault("write", "owner", permissions),
execute: getPermissionOrComputeDefault("execute", "owner", permissions)
},
group: {
read: getPermissionOrComputeDefault("read", "group", permissions),
write: getPermissionOrComputeDefault("write", "group", permissions),
execute: getPermissionOrComputeDefault("execute", "group", permissions)
},
others: {
read: getPermissionOrComputeDefault("read", "others", permissions),
write: getPermissionOrComputeDefault("write", "others", permissions),
execute: getPermissionOrComputeDefault("execute", "others", permissions)
}
};
binaryFlags = permissionsToBinaryFlags(permissions);
} else {
binaryFlags = permissions;
}
return chmodNaive(sourcePath, binaryFlags);
};
const chmodNaive = (fileSystemPath, binaryFlags) => {
return new Promise((resolve, reject) => {
fs.chmod(fileSystemPath, binaryFlags, error => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
};
const actionLevels = {
read: 0,
write: 1,
execute: 2
};
const subjectLevels = {
others: 0,
group: 1,
owner: 2
};
const getPermissionOrComputeDefault = (action, subject, permissions) => {
if (subject in permissions) {
const subjectPermissions = permissions[subject];
if (action in subjectPermissions) {
return subjectPermissions[action];
}
const actionLevel = actionLevels[action];
const actionFallback = Object.keys(actionLevels).find(actionFallbackCandidate => actionLevels[actionFallbackCandidate] > actionLevel && actionFallbackCandidate in subjectPermissions);
if (actionFallback) {
return subjectPermissions[actionFallback];
}
}
const subjectLevel = subjectLevels[subject]; // do we have a subject with a stronger level (group or owner)
// where we could read the action permission ?
const subjectFallback = Object.keys(subjectLevels).find(subjectFallbackCandidate => subjectLevels[subjectFallbackCandidate] > subjectLevel && subjectFallbackCandidate in permissions);
if (subjectFallback) {
const subjectPermissions = permissions[subjectFallback];
return action in subjectPermissions ? subjectPermissions[action] : getPermissionOrComputeDefault(action, subjectFallback, permissions);
}
return false;
};
const isWindows = process.platform === "win32";
const readFileSystemNodeStat = async (source, {
nullIfNotFound = false,
followLink = true
} = {}) => {
if (source.endsWith("/")) source = source.slice(0, -1);
const sourceUrl = assertAndNormalizeFileUrl(source);
const sourcePath = urlToFileSystemPath(sourceUrl);
const handleNotFoundOption = nullIfNotFound ? {
handleNotFoundError: () => null
} : {};
return readStat(sourcePath, {
followLink,
...handleNotFoundOption,
...(isWindows ? {
// Windows can EPERM on stat
handlePermissionDeniedError: async error => {
// unfortunately it means we mutate the permissions
// without being able to restore them to the previous value
// (because reading current permission would also throw)
try {
await writeFileSystemNodePermissions(sourceUrl, 0o666);
const stats = await readStat(sourcePath, {
followLink,
...handleNotFoundOption,
// could not fix the permission error, give up and throw original error
handlePermissionDeniedError: () => {
throw error;
}
});
return stats;
} catch (e) {
// failed to write permission or readState, throw original error as well
throw error;
}
}
} : {})
});
};
const readStat = (sourcePath, {
followLink,
handleNotFoundError = null,
handlePermissionDeniedError = null
} = {}) => {
const nodeMethod = followLink ? fs.stat : fs.lstat;
return new Promise((resolve, reject) => {
nodeMethod(sourcePath, (error, statsObject) => {
if (error) {
if (handlePermissionDeniedError && (error.code === "EPERM" || error.code === "EACCES")) {
resolve(handlePermissionDeniedError(error));
} else if (handleNotFoundError && error.code === "ENOENT") {
resolve(handleNotFoundError(error));
} else {
reject(error);
}
} else {
resolve(statsObject);
}
});
});
};
const createCancellationToken = () => {
const register = callback => {
if (typeof callback !== "function") {
throw new Error(`callback must be a function, got ${callback}`);
}
return {
callback,
unregister: () => {}
};
};
const throwIfRequested = () => undefined;
return {
register,
cancellationRequested: false,
throwIfRequested
};
};
const createOperation = ({
cancellationToken = createCancellationToken(),
start,
...rest
}) => {
const unknownArgumentNames = Object.keys(rest);
if (unknownArgumentNames.length) {
throw new Error(`createOperation called with unknown argument names.
--- unknown argument names ---
${unknownArgumentNames}
--- possible argument names ---
cancellationToken
start`);
}
cancellationToken.throwIfRequested();
const promise = new Promise(resolve => {
resolve(start());
});
const cancelPromise = new Promise((resolve, reject) => {
const cancelRegistration = cancellationToken.register(cancelError => {
cancelRegistration.unregister();
reject(cancelError);
});
promise.then(cancelRegistration.unregister, () => {});
});
const operationPromise = Promise.race([promise, cancelPromise]);
return operationPromise;
};
const createCancelError = reason => {
const cancelError = new Error(`canceled because ${reason}`);
cancelError.name = "CANCEL_ERROR";
cancelError.reason = reason;
return cancelError;
};
const isCancelError = value => {
return value && typeof value === "object" && value.name === "CANCEL_ERROR";
};
const arrayWithout = (array, item) => {
const arrayWithoutItem = [];
let i = 0;
while (i < array.length) {
const value = array[i];
i++;
if (value === item) {
continue;
}
arrayWithoutItem.push(value);
}
return arrayWithoutItem;
};
// https://github.com/tc39/proposal-cancellation/tree/master/stage0
const createCancellationSource = () => {
let requested = false;
let cancelError;
let registrationArray = [];
const cancel = reason => {
if (requested) return;
requested = true;
cancelError = createCancelError(reason);
const registrationArrayCopy = registrationArray.slice();
registrationArray.length = 0;
registrationArrayCopy.forEach(registration => {
registration.callback(cancelError); // const removedDuringCall = registrationArray.indexOf(registration) === -1
});
};
const register = callback => {
if (typeof callback !== "function") {
throw new Error(`callback must be a function, got ${callback}`);
}
const existingRegistration = registrationArray.find(registration => {
return registration.callback === callback;
}); // don't register twice
if (existingRegistration) {
return existingRegistration;
}
const registration = {
callback,
unregister: () => {
registrationArray = arrayWithout(registrationArray, registration);
}
};
registrationArray = [registration, ...registrationArray];
return registration;
};
const throwIfRequested = () => {
if (requested) {
throw cancelError;
}
};
return {
token: {
register,
get cancellationRequested() {
return requested;
},
throwIfRequested
},
cancel
};
};
const getCommandArgument = (argv, name) => {
let i = 0;
while (i < argv.length) {
const arg = argv[i];
if (arg === name) {
return {
name,
index: i,
value: ""
};
}
if (arg.startsWith(`${name}=`)) {
return {
name,
index: i,
value: arg.slice(`${name}=`.length)
};
}
i++;
}
return null;
};
const wrapExternalFunction = (fn, {
catchCancellation = false,
unhandledRejectionStrict = false
} = {}) => {
if (catchCancellation) {
const previousFn = fn;
fn = async () => {
try {
const value = await previousFn();
return value;
} catch (error) {
if (isCancelError(error)) {
// it means consume of the function will resolve with a cancelError
// but when you cancel it means you're not interested in the result anymore
// thanks to this it avoid unhandledRejection
return error;
}
throw error;
}
};
}
if (unhandledRejectionStrict) {
const previousFn = fn;
fn = async () => {
const uninstall = installUnhandledRejectionStrict();
try {
const value = await previousFn();
uninstall();
return value;
} catch (e) {
// don't remove it immediatly to let nodejs emit the unhandled rejection
setTimeout(() => {
uninstall();
});
throw e;
}
};
}
return fn();
};
const installUnhandledRejectionStrict = () => {
const unhandledRejectionArg = getCommandArgument(process.execArgv, "--unhandled-rejections");
if (unhandledRejectionArg === "strict") return () => {};
const onUnhandledRejection = reason => {
throw reason;
};
process.once("unhandledRejection", onUnhandledRejection);
return () => {
process.removeListener("unhandledRejection", onUnhandledRejection);
};
};
const catchCancellation = asyncFn => wrapExternalFunction(asyncFn, {
catchCancellation: true
});
const readDirectory = async (url, {
emfileMaxWait = 1000
} = {}) => {
const directoryUrl = assertAndNormalizeDirectoryUrl(url);
const directoryPath = urlToFileSystemPath(directoryUrl);
const startMs = Date.now();
let attemptCount = 0;
const attempt = () => {
return readdirNaive(directoryPath, {
handleTooManyFilesOpenedError: async error => {
attemptCount++;
const nowMs = Date.now();
const timeSpentWaiting = nowMs - startMs;
if (timeSpentWaiting > emfileMaxWait) {
throw error;
}
return new Promise(resolve => {
setTimeout(() => {
resolve(attempt());
}, attemptCount);
});
}
});
};
return attempt();
};
const readdirNaive = (directoryPath, {
handleTooManyFilesOpenedError = null
} = {}) => {
return new Promise((resolve, reject) => {
fs.readdir(directoryPath, (error, names) => {
if (error) {
// https://nodejs.org/dist/latest-v13.x/docs/api/errors.html#errors_common_system_errors
if (handleTooManyFilesOpenedError && (error.code === "EMFILE" || error.code === "ENFILE")) {
resolve(handleTooManyFilesOpenedError(error));
} else {
reject(error);
}
} else {
resolve(names);
}
});
});
};
const getCommonPathname = (pathname, otherPathname) => {
const firstDifferentCharacterIndex = findFirstDifferentCharacterIndex(pathname, otherPathname); // pathname and otherpathname are exactly the same
if (firstDifferentCharacterIndex === -1) {
return pathname;
}
const commonString = pathname.slice(0, firstDifferentCharacterIndex + 1); // the first different char is at firstDifferentCharacterIndex
if (pathname.charAt(firstDifferentCharacterIndex) === "/") {
return commonString;
}
if (otherPathname.charAt(firstDifferentCharacterIndex) === "/") {
return commonString;
}
const firstDifferentSlashIndex = commonString.lastIndexOf("/");
return pathname.slice(0, firstDifferentSlashIndex + 1);
};
const findFirstDifferentCharacterIndex = (string, otherString) => {
const maxCommonLength = Math.min(string.length, otherString.length);
let i = 0;
while (i < maxCommonLength) {
const char = string.charAt(i);
const otherChar = otherString.charAt(i);
if (char !== otherChar) {
return i;
}
i++;
}
if (string.length === otherString.length) {
return -1;
} // they differ at maxCommonLength
return maxCommonLength;
};
const pathnameToDirectoryPathname = pathname => {
if (pathname.endsWith("/")) {
return pathname;
}
const slashLastIndex = pathname.lastIndexOf("/");
if (slashLastIndex === -1) {
return "";
}
return pathname.slice(0, slashLastIndex + 1);
};
const urlToRelativeUrl = (urlArg, baseUrlArg) => {
const url = new URL(urlArg);
const baseUrl = new URL(baseUrlArg);
if (url.protocol !== baseUrl.protocol) {
return urlArg;
}
if (url.username !== baseUrl.username || url.password !== baseUrl.password) {
return urlArg.slice(url.protocol.length);
}
if (url.host !== baseUrl.host) {
return urlArg.slice(url.protocol.length);
}
const {
pathname,
hash,
search
} = url;
if (pathname === "/") {
return baseUrl.pathname.slice(1);
}
const {
pathname: basePathname
} = baseUrl;
const commonPathname = getCommonPathname(pathname, basePathname);
if (!commonPathname) {
return urlArg;
}
const specificPathname = pathname.slice(commonPathname.length);
const baseSpecificPathname = basePathname.slice(commonPathname.length);
const baseSpecificDirectoryPathname = pathnameToDirectoryPathname(baseSpecificPathname);
const relativeDirectoriesNotation = baseSpecificDirectoryPathname.replace(/.*?\//g, "../");
const relativePathname = `${relativeDirectoriesNotation}${specificPathname}`;
return `${relativePathname}${search}${hash}`;
};
const comparePathnames = (leftPathame, rightPathname) => {
const leftPartArray = leftPathame.split("/");
const rightPartArray = rightPathname.split("/");
const leftLength = leftPartArray.length;
const rightLength = rightPartArray.length;
const maxLength = Math.max(leftLength, rightLength);
let i = 0;
while (i < maxLength) {
const leftPartExists = (i in leftPartArray);
const rightPartExists = (i in rightPartArray); // longer comes first
if (!leftPartExists) return +1;
if (!rightPartExists) return -1;
const leftPartIsLast = i === leftPartArray.length - 1;
const rightPartIsLast = i === rightPartArray.length - 1; // folder comes first
if (leftPartIsLast && !rightPartIsLast) return +1;
if (!leftPartIsLast && rightPartIsLast) return -1;
const leftPart = leftPartArray[i];
const rightPart = rightPartArray[i];
i++; // local comparison comes first
const comparison = leftPart.localeCompare(rightPart);
if (comparison !== 0) return comparison;
}
if (leftLength < rightLength) return +1;
if (leftLength > rightLength) return -1;
return 0;
};
const collectFiles = async ({
cancellationToken = createCancellationToken(),
directoryUrl,
specifierMetaMap,
predicate,
matchingFileOperation = () => null
}) => {
const rootDirectoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
if (typeof predicate !== "function") {
throw new TypeError(`predicate must be a function, got ${predicate}`);
}
if (typeof matchingFileOperation !== "function") {
throw new TypeError(`matchingFileOperation must be a function, got ${matchingFileOperation}`);
}
const specifierMetaMapNormalized = normalizeSpecifierMetaMap(specifierMetaMap, rootDirectoryUrl);
const matchingFileResultArray = [];
const visitDirectory = async directoryUrl => {
const directoryItems = await createOperation({
cancellationToken,
start: () => readDirectory(directoryUrl)
});
await Promise.all(directoryItems.map(async directoryItem => {
const directoryChildNodeUrl = `${directoryUrl}${directoryItem}`;
const directoryChildNodeStats = await createOperation({
cancellationToken,
start: () => readFileSystemNodeStat(directoryChildNodeUrl, {
// we ignore symlink because recursively traversed
// so symlinked file will be discovered.
// Moreover if they lead outside of directoryPath it can become a problem
// like infinite recursion of whatever.
// that we could handle using an object of pathname already seen but it will be useless
// because directoryPath is recursively traversed
followLink: false
})
});
if (directoryChildNodeStats.isDirectory()) {
const subDirectoryUrl = `${directoryChildNodeUrl}/`;
if (!urlCanContainsMetaMatching({
url: subDirectoryUrl,
specifierMetaMap: specifierMetaMapNormalized,
predicate
})) {
return;
}
await visitDirectory(subDirectoryUrl);
return;
}
if (directoryChildNodeStats.isFile()) {
const meta = urlToMeta({
url: directoryChildNodeUrl,
specifierMetaMap: specifierMetaMapNormalized
});
if (!predicate(meta)) return;
const relativeUrl = urlToRelativeUrl(directoryChildNodeUrl, rootDirectoryUrl);
const operationResult = await createOperation({
cancellationToken,
start: () => matchingFileOperation({
cancellationToken,
relativeUrl,
meta,
fileStats: directoryChildNodeStats
})
});
matchingFileResultArray.push({
relativeUrl,
meta,
fileStats: directoryChildNodeStats,
operationResult
});
return;
}
}));
};
await visitDirectory(rootDirectoryUrl); // When we operate on thoose files later it feels more natural
// to perform operation in the same order they appear in the filesystem.
// It also allow to get a predictable return value.
// For that reason we sort matchingFileResultArray
matchingFileResultArray.sort((leftFile, rightFile) => {
return comparePathnames(leftFile.relativeUrl, rightFile.relativeUrl);
});
return matchingFileResultArray;
};
const {
mkdir
} = fs.promises;
const writeDirectory = async (destination, {
recursive = true,
allowUseless = false
} = {}) => {
const destinationUrl = assertAndNormalizeDirectoryUrl(destination);
const destinationPath = urlToFileSystemPath(destinationUrl);
const destinationStats = await readFileSystemNodeStat(destinationUrl, {
nullIfNotFound: true,
followLink: false
});
if (destinationStats) {
if (destinationStats.isDirectory()) {
if (allowUseless) {
return;
}
throw new Error(`directory already exists at ${destinationPath}`);
}
const destinationType = statsToType(destinationStats);
throw new Error(`cannot write directory at ${destinationPath} because there is a ${destinationType}`);
}
try {
await mkdir(destinationPath, {
recursive
});
} catch (error) {
if (allowUseless && error.code === "EEXIST") {
return;
}
throw error;
}
};
const resolveUrl = (specifier, baseUrl) => {
if (typeof baseUrl === "undefined") {
throw new TypeError(`baseUrl missing to resolve ${specifier}`);
}
return String(new URL(specifier, baseUrl));
};
const ensureParentDirectories = async destination => {
const destinationUrl = assertAndNormalizeFileUrl(destination);
const destinationPath = urlToFileSystemPath(destinationUrl);
const destinationParentPath = path.dirname(destinationPath);
return writeDirectory(destinationParentPath, {
recursive: true,
allowUseless: true
});
};
const isWindows$1 = process.platform === "win32";
const baseUrlFallback = fileSystemPathToUrl(process.cwd());
const isWindows$2 = process.platform === "win32";
const addCallback = callback => {
const triggerHangUpOrDeath = () => callback(); // SIGHUP http://man7.org/linux/man-pages/man7/signal.7.html
process.once("SIGUP", triggerHangUpOrDeath);
return () => {
process.removeListener("SIGUP", triggerHangUpOrDeath);
};
};
const SIGUPSignal = {
addCallback
};
const addCallback$1 = callback => {
// SIGINT is CTRL+C from keyboard also refered as keyboard interruption
// http://man7.org/linux/man-pages/man7/signal.7.html
// may also be sent by vscode https://github.com/Microsoft/vscode-node-debug/issues/1#issuecomment-405185642
process.once("SIGINT", callback);
return () => {
process.removeListener("SIGINT", callback);
};
};
const SIGINTSignal = {
addCallback: addCallback$1
};
const addCallback$2 = callback => {
if (process.platform === "win32") {
console.warn(`SIGTERM is not supported on windows`);
return () => {};
}
const triggerTermination = () => callback(); // SIGTERM http://man7.org/linux/man-pages/man7/signal.7.html
process.once("SIGTERM", triggerTermination);
return () => {
process.removeListener("SIGTERM", triggerTermination);
};
};
const SIGTERMSignal = {
addCallback: addCallback$2
};
let beforeExitCallbackArray = [];
let uninstall;
const addCallback$3 = callback => {
if (beforeExitCallbackArray.length === 0) uninstall = install();
beforeExitCallbackArray = [...beforeExitCallbackArray, callback];
return () => {
if (beforeExitCallbackArray.length === 0) return;
beforeExitCallbackArray = beforeExitCallbackArray.filter(beforeExitCallback => beforeExitCallback !== callback);
if (beforeExitCallbackArray.length === 0) uninstall();
};
};
const install = () => {
const onBeforeExit = () => {
return beforeExitCallbackArray.reduce(async (previous, callback) => {
await previous;
return callback();
}, Promise.resolve());
};
process.once("beforeExit", onBeforeExit);
return () => {
process.removeListener("beforeExit", onBeforeExit);
};
};
const beforeExitSignal = {
addCallback: addCallback$3
};
const addCallback$4 = (callback, {
collectExceptions = false
} = {}) => {
if (!collectExceptions) {
const exitCallback = () => {
callback();
};
process.on("exit", exitCallback);
return () => {
process.removeListener("exit", exitCallback);
};
}
const {
getExceptions,
stop
} = trackExceptions();
const exitCallback = () => {
process.removeListener("exit", exitCallback);
stop();
callback({
exceptionArray: getExceptions().map(({
exception,
origin
}) => {
return {
exception,
origin
};
})
});
};
process.on("exit", exitCallback);
return () => {
process.removeListener("exit", exitCallback);
};
};
const trackExceptions = () => {
let exceptionArray = [];
const unhandledRejectionCallback = (unhandledRejection, promise) => {
exceptionArray = [...exceptionArray, {
origin: "unhandledRejection",
exception: unhandledRejection,
promise
}];
};
const rejectionHandledCallback = promise => {
exceptionArray = exceptionArray.filter(exceptionArray => exceptionArray.promise !== promise);
};
const uncaughtExceptionCallback = (uncaughtException, origin) => {
// since node 12.4 https://nodejs.org/docs/latest-v12.x/api/process.html#process_event_uncaughtexception
if (origin === "unhandledRejection") return;
exceptionArray = [...exceptionArray, {
origin: "uncaughtException",
exception: uncaughtException
}];
};
process.on("unhandledRejection", unhandledRejectionCallback);
process.on("rejectionHandled", rejectionHandledCallback);
process.on("uncaughtException", uncaughtExceptionCallback);
return {
getExceptions: () => exceptionArray,
stop: () => {
process.removeListener("unhandledRejection", unhandledRejectionCallback);
process.removeListener("rejectionHandled", rejectionHandledCallback);
process.removeListener("uncaughtException", uncaughtExceptionCallback);
}
};
};
const exitSignal = {
addCallback: addCallback$4
};
const addCallback$5 = callback => {
return eventRace({
SIGHUP: {
register: SIGUPSignal.addCallback,
callback: () => callback("SIGHUP")
},
SIGINT: {
register: SIGINTSignal.addCallback,
callback: () => callback("SIGINT")
},
...(process.platform === "win32" ? {} : {
SIGTERM: {
register: SIGTERMSignal.addCallback,
callback: () => callback("SIGTERM")
}
}),
beforeExit: {
register: beforeExitSignal.addCallback,
callback: () => callback("beforeExit")
},
exit: {
register: exitSignal.addCallback,
callback: () => callback("exit")
}
});
};
const eventRace = eventMap => {
const unregisterMap = {};
const unregisterAll = reason => {
return Object.keys(unregisterMap).map(name => unregisterMap[name](reason));
};
Object.keys(eventMap).forEach(name => {
const {
register,
callback
} = eventMap[name];
unregisterMap[name] = register((...args) => {
unregisterAll();
callback(...args);
});
});
return unregisterAll;
};
const teardownSignal = {
addCallback: addCallback$5
};
const createCancellationTokenForProcess = () => {
const teardownCancelSource = createCancellationSource();
teardownSignal.addCallback(reason => teardownCancelSource.cancel(`process ${reason}`));
return teardownCancelSource.token;
};
const readFilePromisified = util.promisify(fs.readFile);
const readFile = async value => {
const fileUrl = assertAndNormalizeFileUrl(value);
const filePath = urlToFileSystemPath(fileUrl);
const buffer = await readFilePromisified(filePath);
return buffer.toString();
};
const isWindows$3 = process.platform === "win32";
/* eslint-disable import/max-dependencies */
const isLinux = process.platform === "linux"; // linux does not support recursive option
const {
writeFile: writeFileNode
} = fs.promises;
const writeFile = async (destination, content = "") => {
const destinationUrl = assertAndNormalizeFileUrl(destination);
const destinationPath = urlToFileSystemPath(destinationUrl);
try {
await writeFileNode(destinationPath, content);
} catch (error) {
if (error.code === "ENOENT") {
await ensureParentDirectories(destinationUrl);
await writeFileNode(destinationPath, content);
return;
}
throw error;
}
};
const LOG_LEVEL_OFF = "off";
const LOG_LEVEL_DEBUG = "debug";
const LOG_LEVEL_INFO = "info";
const LOG_LEVEL_WARN = "warn";
const LOG_LEVEL_ERROR = "error";
const createLogger = ({
logLevel = LOG_LEVEL_INFO
} = {}) => {
if (logLevel === LOG_LEVEL_DEBUG) {
return {
debug,
info,
warn,
error
};
}
if (logLevel === LOG_LEVEL_INFO) {
return {
debug: debugDisabled,
info,
warn,
error
};
}
if (logLevel === LOG_LEVEL_WARN) {
return {
debug: debugDisabled,
info: infoDisabled,
warn,
error
};
}
if (logLevel === LOG_LEVEL_ERROR) {
return {
debug: debugDisabled,
info: infoDisabled,
warn: warnDisabled,
error
};
}
if (logLevel === LOG_LEVEL_OFF) {
return {
debug: debugDisabled,
info: infoDisabled,
warn: warnDisabled,
error: errorDisabled
};
}
throw new Error(`unexpected logLevel.
--- logLevel ---
${logLevel}
--- allowed log levels ---
${LOG_LEVEL_OFF}
${LOG_LEVEL_ERROR}
${LOG_LEVEL_WARN}
${LOG_LEVEL_INFO}
${LOG_LEVEL_DEBUG}`);
};
const debug = console.debug;
const debugDisabled = () => {};
const info = console.info;
const infoDisabled = () => {};
const warn = console.warn;
const warnDisabled = () => {};
const error = console.error;
const errorDisabled = () => {};
const STATUS_NOT_SUPPORTED = "not-supported";
const STATUS_IGNORED = "ignored";
const STATUS_PRETTY = "pretty";
const STATUS_UGLY = "ugly";
const STATUS_ERRORED = "errored";
const STATUS_FORMATTED = "formatted";
const close = "\x1b[0m";
const green = "\x1b[32m";
const red = "\x1b[31m";
const blue = "\x1b[34m";
const yellow = "\x1b[33m";
const magenta = "\x1b[35m"; // const grey = "\x1b[39m"
const erroredStyle = string => `${red}${string}${close}`;
const erroredStyleWithIcon = string => erroredStyle(`${erroredIcon} ${string}`);
const erroredIcon = "\u2613"; // cross ☓
const notSupportedStyle = string => `${magenta}${string}${close}`;
const notSupportedStyleWithIcon = string => notSupportedStyle(`${notSupportedIcon} ${string}`);
const notSupportedIcon = "\u2714"; // checkmark ✔
const ignoredStyle = string => `${magenta}${string}${close}`;
const ignoredStyleWithIcon = string => ignoredStyle(`${ignoredIcon} ${string}`);
const ignoredIcon = "\u2714"; // checkmark ✔
// const ignoredIcon = "\u003F" // question mark ?
const uglyStyle = string => `${yellow}${string}${close}`;
const uglyStyleWithIcon = string => uglyStyle(`${uglyIcon} ${string}`);
const uglyIcon = "\u2613"; // cross ☓
const prettyStyle = string => `${green}${string}${close}`;
const prettyStyleWithIcon = string => prettyStyle(`${prettyIcon} ${string}`);
const prettyIcon = "\u2714"; // checkmark ✔
const formattedStyle = string => `${blue}${string}${close}`;
const formattedStyleWithIcon = string => formattedStyle(`${formattedIcon} ${string}`);
const formattedIcon = "\u2714"; // checkmark ✔
const createSummaryLog = ({
totalCount,
...rest
}) => {
if (totalCount === 0) return `
done.`;
return `
${createSummaryDetails({
totalCount,
...rest
})}`;
};
const createSummaryDetails = ({
totalCount,
ignoredCount,
notSupportedCount,
erroredCount,
uglyCount,
formattedCount,
prettyCount
}) => {
if (prettyCount === totalCount) {
return `all ${prettyStyle("already formatted")}`;
}
if (formattedCount === totalCount) {
return `all ${formattedStyle("formatted")}`;
}
if (erroredCount === totalCount) {
return `all ${erroredStyle("errored")}`;
}
if (ignoredCount + notSupportedCount === totalCount) {
return `all ${ignoredStyle("ignored or not supported")}`;
}
if (uglyCount === totalCount) {
return `all ${uglyStyle("needs formatting")}`;
}
return createMixedDetails({
ignoredCount,
notSupportedCount,
erroredCount,
uglyCount,
formattedCount,
prettyCount
});
};
const createMixedDetails = ({
ignoredCount,
notSupportedCount,
erroredCount,
uglyCount,
formattedCount,
prettyCount
}) => {
const parts = [];
if (erroredCount) {
parts.push(`${erroredCount} ${erroredStyle("errored")}`);
}
if (formattedCount) {
parts.push(`${formattedCount} ${formattedStyle("formatted")}`);
}
if (prettyCount) {
parts.push(`${prettyCount} ${prettyStyle("already formatted")}`);
}
if (ignoredCount || notSupportedCount) {
parts.push(`${ignoredCount + notSupportedCount} ${ignoredStyle("ignored or not supported")}`);
}
if (uglyCount) {
parts.push(`${uglyCount} ${uglyStyle("needs formatting")}`);
}
return `${parts.join(", ")}.`;
};
const createIgnoredFileLog = ({
relativeUrl
}) => `
${relativeUrl} -> ${ignoredStyleWithIcon("ignored")}`;
const createNotSupportedFileLog = ({
relativeUrl
}) => `
${relativeUrl} -> ${notSupportedStyleWithIcon("not supported")}`;
const createErroredFileLog = ({
relativeUrl,
statusDetail
}) => `
${relativeUrl} -> ${erroredStyleWithIcon("errored")}
${statusDetail}`;
const createUglyFileLog = ({
relativeUrl
}) => `
${relativeUrl} -> ${uglyStyleWithIcon("needs formatting")}`;
const createFormattedFileLog = ({
relativeUrl
}) => `
${relativeUrl} -> ${formattedStyleWithIcon("formatted")}`;
const createPrettyFileLog = ({
relativeUrl
}) => `
${relativeUrl} -> ${prettyStyleWithIcon("already formatted")}`;
const collectStagedFiles = async ({
projectDirectoryUrl,
specifierMetaMap,
predicate
}) => {
// https://git-scm.com/docs/git-diff
const gitDiffOutput = await runCommand("git diff --staged --name-only --diff-filter=AM");
const stagedFiles = gitDiffOutput.trim().split(/\r?\n/);
const specifierMetaMapNormalized = normalizeSpecifierMetaMap(specifierMetaMap, projectDirectoryUrl);
return stagedFiles.filter(relativePath => {
return predicate(urlToMeta({
url: resolveUrl(relativePath, projectDirectoryUrl),
specifierMetaMap: specifierMetaMapNormalized
}));
});
};
const runCommand = cmd => {
return new Promise((resolve, reject) => {
child_process.exec(cmd, (error, stdout, stderr) => {
if (error) {
// sometimes (e.g. eslint) we have a meaningful stdout along with the stderr
reject(stdout ? `${stdout}\n\n${stderr}` : stderr);
} else {
resolve(stdout);
}
});
});
};
const collectProjectFiles = async ({
cancellationToken,
projectDirectoryUrl,
specifierMetaMap,
predicate
}) => {
const files = await collectFiles({
cancellationToken,
directoryUrl: projectDirectoryUrl,
specifierMetaMap,
predicate
});
return files.map(({
relativeUrl
}) => relativeUrl);
};
/**
* This object decribes th usual files structure used in jsenv projects.
*
* It means files inside .github/, docs/, src/, test/ and root files
* will be formatted with prettier when supported.
*
* If a project has an other directory containing files that should be formatted
* by prettier it can be added by doing
*
* {
* ...jsenvProjectFilesConfig,
* './directory/': true
* }
*
*/
const jsenvProjectFilesConfig = {
"./.github/": true,
"./docs/": true,
"./src/": true,
"./test/": true,
"./script/": true,
"./*": true,
/