@jsenv/util
Advanced tools
Comparing version 3.1.0 to 3.2.0
@@ -11,2 +11,449 @@ 'use strict'; | ||
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 | ||
return pass({ | ||
patternIndex: patternIndex + 1, | ||
index: string.length | ||
}); | ||
} // 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 => { | ||
if (!isPlainObject(value)) { | ||
throw new TypeError(`specifierMetaMap must be a plain object, got ${value}`); | ||
} // we could ensure it's key/value pair of url like key/object or null values | ||
}; | ||
const normalizeSpecifierMetaMap = (specifierMetaMap, url, ...rest) => { | ||
assertSpecifierMetaMap(specifierMetaMap); | ||
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 => { | ||
@@ -400,40 +847,52 @@ return url.endsWith("/") ? url : `${url}/`; | ||
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 | ||
}); | ||
const createCancellationToken = () => { | ||
const register = callback => { | ||
if (typeof callback !== "function") { | ||
throw new Error(`callback must be a function, got ${callback}`); | ||
} | ||
if (destinationStats) { | ||
if (destinationStats.isDirectory()) { | ||
if (allowUseless) { | ||
return; | ||
} | ||
return { | ||
callback, | ||
unregister: () => {} | ||
}; | ||
}; | ||
throw new Error(`directory already exists at ${destinationPath}`); | ||
} | ||
const throwIfRequested = () => undefined; | ||
const destinationType = statsToType(destinationStats); | ||
throw new Error(`cannot write directory at ${destinationPath} because there is a ${destinationType}`); | ||
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`); | ||
} | ||
try { | ||
await mkdir(destinationPath, { | ||
recursive | ||
cancellationToken.throwIfRequested(); | ||
const promise = new Promise(resolve => { | ||
resolve(start()); | ||
}); | ||
const cancelPromise = new Promise((resolve, reject) => { | ||
const cancelRegistration = cancellationToken.register(cancelError => { | ||
cancelRegistration.unregister(); | ||
reject(cancelError); | ||
}); | ||
} catch (error) { | ||
if (allowUseless && error.code === "EEXIST") { | ||
return; | ||
} | ||
throw error; | ||
} | ||
promise.then(cancelRegistration.unregister, () => {}); | ||
}); | ||
const operationPromise = Promise.race([promise, cancelPromise]); | ||
return operationPromise; | ||
}; | ||
@@ -491,2 +950,268 @@ | ||
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) => { | ||
@@ -790,103 +1515,2 @@ if (typeof baseUrl === "undefined") { | ||
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 ensureParentDirectories = async destination => { | ||
@@ -1390,2 +2014,3 @@ const destinationUrl = assertAndNormalizeFileUrl(destination); | ||
exports.applySpecifierPatternMatching = applySpecifierPatternMatching; | ||
exports.assertAndNormalizeDirectoryUrl = assertAndNormalizeDirectoryUrl; | ||
@@ -1396,2 +2021,4 @@ exports.assertAndNormalizeFileUrl = assertAndNormalizeFileUrl; | ||
exports.bufferToEtag = bufferToEtag; | ||
exports.collectFiles = collectFiles; | ||
exports.comparePathnames = comparePathnames; | ||
exports.copyFileSystemNode = copyFileSystemNode; | ||
@@ -1404,3 +2031,5 @@ exports.ensureEmptyDirectory = ensureEmptyDirectory; | ||
exports.isFileSystemPath = isFileSystemPath; | ||
exports.metaMapToSpecifierMetaMap = metaMapToSpecifierMetaMap; | ||
exports.moveFileSystemNode = moveFileSystemNode; | ||
exports.normalizeSpecifierMetaMap = normalizeSpecifierMetaMap; | ||
exports.readDirectory = readDirectory; | ||
@@ -1416,4 +2045,6 @@ exports.readFile = readFile; | ||
exports.testFileSystemNodePermissions = testFileSystemNodePermissions; | ||
exports.urlCanContainsMetaMatching = urlCanContainsMetaMatching; | ||
exports.urlIsInsideOf = urlIsInsideOf; | ||
exports.urlToFileSystemPath = urlToFileSystemPath; | ||
exports.urlToMeta = urlToMeta; | ||
exports.urlToRelativeUrl = urlToRelativeUrl; | ||
@@ -1420,0 +2051,0 @@ exports.writeDirectory = writeDirectory; |
13
index.js
@@ -0,1 +1,12 @@ | ||
// we won't internalize @jsenv/url-meta | ||
// so that @jsenv/url-meta does not becomes nodejs specific | ||
// but there functions could be inside this repository | ||
export { | ||
applySpecifierPatternMatching, | ||
metaMapToSpecifierMetaMap, | ||
normalizeSpecifierMetaMap, | ||
urlCanContainsMetaMatching, | ||
urlToMeta, | ||
} from "@jsenv/url-meta" | ||
export { assertAndNormalizeDirectoryUrl } from "./src/assertAndNormalizeDirectoryUrl.js" | ||
@@ -6,2 +17,4 @@ export { assertAndNormalizeFileUrl } from "./src/assertAndNormalizeFileUrl.js" | ||
export { bufferToEtag } from "./src/bufferToEtag.js" | ||
export { collectFiles } from "./src/collectFiles.js" | ||
export { comparePathnames } from "./src/comparePathnames.js" | ||
export { ensureEmptyDirectory } from "./src/ensureEmptyDirectory.js" | ||
@@ -8,0 +21,0 @@ export { ensureWindowsDriveLetter } from "./src/ensureWindowsDriveLetter.js" |
{ | ||
"name": "@jsenv/util", | ||
"version": "3.1.0", | ||
"version": "3.2.0", | ||
"description": "Set of functions often needed when using Node.js.", | ||
@@ -45,3 +45,6 @@ "license": "MIT", | ||
}, | ||
"dependencies": {}, | ||
"dependencies": { | ||
"@jsenv/cancellation": "1.3.0", | ||
"@jsenv/url-meta": "5.1.0" | ||
}, | ||
"devDependencies": { | ||
@@ -52,2 +55,3 @@ "@jsenv/assert": "1.2.1", | ||
"@jsenv/eslint-config": "12.1.0", | ||
"@jsenv/git-hooks": "1.2.0", | ||
"@jsenv/github-release-package": "1.1.1", | ||
@@ -54,0 +58,0 @@ "@jsenv/node-module-import-map": "10.0.1", |
@@ -22,2 +22,4 @@ # util | ||
- [bufferToEtag](#bufferToEtag) | ||
- [collectFiles](#collectFiles) | ||
- [comparePathnames](#comparePathnames) | ||
- [copyFileSystemNode](#copyFileSystemNode) | ||
@@ -163,2 +165,37 @@ - [ensureEmptyDirectory](#ensureEmptyDirectory) | ||
### collectFiles | ||
`collectFiles` is an async function collectings a subset of files inside a directory. | ||
```js | ||
import { collectFiles } from "@jsenv/util" | ||
const files = await collectFiles({ | ||
directoryUrl: "file:///Users/you/directory", | ||
specifierMetaMap: { | ||
"./**/*.js": { | ||
whatever: 42, | ||
}, | ||
}, | ||
predicate: (meta) => { | ||
return meta.whatever === 42 | ||
}, | ||
}) | ||
``` | ||
— source code at [src/collectFiles.js](./src/collectFiles.js). | ||
### comparePathnames | ||
`comparePathnames` is a function compare two pathnames and returning which pathnames comes first in a filesystem. | ||
```js | ||
import { comparePathnames } from "@jsenv/util" | ||
const pathnames = ["a/b.js", "a.js"] | ||
pathnames.sort(comparePathnames) | ||
``` | ||
— source code at [src/comparePathnames.js](./src/comparePathnames.js). | ||
### copyFileSystemNode | ||
@@ -165,0 +202,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
191593
47
3240
527
2
14
+ Added@jsenv/cancellation@1.3.0
+ Added@jsenv/url-meta@5.1.0
+ Added@jsenv/cancellation@1.3.0(transitive)
+ Added@jsenv/url-meta@5.1.0(transitive)