UNPKG

@jsenv/prettier-check-project

Version:
1,901 lines (1,593 loc) 56.8 kB
'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, /