UNPKG

cspell-io

Version:

A library of useful I/O functions used across various cspell tools.

1,524 lines (1,488 loc) 49.5 kB
import { isFileURL, isUrlLike, toFileURL, toURL, urlBasename, urlParent as urlDirname } from "@cspell/url"; import * as zlib from "node:zlib"; import { gunzipSync, gzip } from "node:zlib"; import { ServiceBus, createResponse, createResponseFail, isServiceResponseSuccess, requestFactory } from "@cspell/cspell-service-bus"; import * as fs from "node:fs"; import { promises, readFileSync, statSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import * as Stream from "node:stream"; import assert from "node:assert"; //#region src/async/asyncIterable.ts /** * Reads an entire iterable and converts it into a promise. * @param asyncIterable the async iterable to wait for. */ async function toArray(asyncIterable) { const data = []; for await (const item of asyncIterable) data.push(item); return data; } //#endregion //#region src/common/CFileReference.ts var CFileReference = class CFileReference { /** * Use to ensure the nominal type separation between CFileReference and FileReference * See: https://github.com/microsoft/TypeScript/wiki/FAQ#when-and-why-are-classes-nominal */ _; gz; constructor(url, encoding, baseFilename, gz) { this.url = url; this.encoding = encoding; this.baseFilename = baseFilename; this.gz = gz ?? (baseFilename?.endsWith(".gz") || void 0) ?? (url.pathname.endsWith(".gz") || void 0); } static isCFileReference(obj) { return obj instanceof CFileReference; } static from(fileReference, encoding, baseFilename, gz) { if (CFileReference.isCFileReference(fileReference)) return fileReference; if (fileReference instanceof URL) return new CFileReference(fileReference, encoding, baseFilename, gz); return new CFileReference(fileReference.url, fileReference.encoding, fileReference.baseFilename, fileReference.gz); } toJson() { return { url: this.url.href, encoding: this.encoding, baseFilename: this.baseFilename, gz: this.gz }; } }; /** * * @param file - a URL, file path, or FileReference * @param encoding - optional encoding used to decode the file. * @param baseFilename - optional base filename used with data URLs. * @param gz - optional flag to indicate if the file is gzipped. * @returns a FileReference */ function toFileReference(file, encoding, baseFilename, gz) { const fileReference = typeof file === "string" ? toFileURL(file) : file; if (fileReference instanceof URL) return new CFileReference(fileReference, encoding, baseFilename, gz); return CFileReference.from(fileReference); } function isFileReference(ref) { return CFileReference.isCFileReference(ref) || !(ref instanceof URL) && typeof ref !== "string"; } function renameFileReference(ref, newUrl) { return new CFileReference(newUrl, ref.encoding, ref.baseFilename, ref.gz); } function toFileResourceRequest(file, encoding, signal) { const fileReference = typeof file === "string" ? toFileURL(file) : file; if (fileReference instanceof URL) return { url: fileReference, encoding, signal }; return { url: fileReference.url, encoding: encoding ?? fileReference.encoding, signal }; } //#endregion //#region src/errors/errors.ts var ErrorNotImplemented = class extends Error { constructor(method, options) { super(`Method ${method} is not supported.`, options); this.method = method; } }; var AssertionError = class extends Error { constructor(message, options) { super(message, options); this.message = message; } }; //#endregion //#region src/errors/assert.ts function assert$1(value, message) { if (!value) throw new AssertionError(message ?? "Assertion failed"); } //#endregion //#region src/common/arrayBuffers.ts /** * Treat a ArrayBufferView as a Uint8Array. * The Uint8Array will share the same underlying ArrayBuffer. * @param data - source data * @returns Uint8Array */ function asUint8Array(data) { return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); } function arrayBufferViewToBuffer(data) { if (data instanceof Buffer) return data; const buf = Buffer.from(data.buffer); if (data.byteOffset === 0 && data.byteLength === data.buffer.byteLength) return buf; return buf.subarray(data.byteOffset, data.byteOffset + data.byteLength); } /** * Copy the data buffer. * @param data - source data * @returns A copy of the data */ function copyArrayBufferView(data) { return new Uint8Array(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)); } /** * Swap the bytes in a buffer. * @param data - data to swap * @returns data */ function swap16Poly(data) { const view = new DataView(data.buffer, data.byteOffset, data.byteLength); for (let i = 0; i < view.byteLength; i += 2) view.setUint16(i, view.getUint16(i, false), true); return data; } /** * Swap the bytes in a buffer. * @param data - data to swap * @returns data */ function swap16(data) { if (typeof Buffer !== "undefined") return arrayBufferViewToBuffer(data).swap16(); return swap16Poly(data); } function swapBytes(data) { const buf = copyArrayBufferView(data); return swap16(buf); } //#endregion //#region src/common/encode-decode.ts const BOM_BE = 65279; const BOM_LE = 65534; const decoderUTF8 = new TextDecoder("utf8"); const decoderUTF16LE = new TextDecoder("utf-16le"); const decoderUTF16BE = createTextDecoderUtf16BE(); const encoderUTF8 = new TextEncoder(); function decodeUtf16LE(data) { const buf = asUint8Array(data); const bom = buf[0] << 8 | buf[1]; return decoderUTF16LE.decode(bom === BOM_LE ? buf.subarray(2) : buf); } function decodeUtf16BE(data) { const buf = asUint8Array(data); const bom = buf[0] << 8 | buf[1]; return decoderUTF16BE.decode(bom === BOM_BE ? buf.subarray(2) : buf); } function decodeToString(data, encoding) { if (isGZipped(data)) return decodeToString(decompressBuffer(data), encoding); const buf = asUint8Array(data); const bom = buf[0] << 8 | buf[1]; if (bom === BOM_BE || buf[0] === 0 && buf[1] !== 0) return decodeUtf16BE(buf); if (bom === BOM_LE || buf[0] !== 0 && buf[1] === 0) return decodeUtf16LE(buf); if (!encoding) return decoderUTF8.decode(buf); switch (encoding) { case "utf-16be": case "utf16be": return decodeUtf16BE(buf); case "utf-16le": case "utf16le": return decodeUtf16LE(buf); case "utf-8": case "utf8": return decoderUTF8.decode(buf); } throw new UnsupportedEncodingError(encoding); } function decode(data, encoding) { switch (encoding) { case "base64": case "base64url": case "hex": return arrayBufferViewToBuffer(data).toString(encoding); } const result = decodeToString(data, encoding); return result; } function encodeString(str, encoding, bom) { switch (encoding) { case void 0: case "utf-8": case "utf8": return encoderUTF8.encode(str); case "utf-16be": case "utf16be": return encodeUtf16BE(str, bom); case "utf-16le": case "utf16le": return encodeUtf16LE(str, bom); } return Buffer.from(str, encoding); } function encodeUtf16LE(str, bom = true) { const buf = Buffer.from(str, "utf16le"); if (bom) { const target = Buffer.alloc(buf.length + 2); target.writeUint16LE(BOM_BE); buf.copy(target, 2); return target; } return buf; } function encodeUtf16BE(str, bom = true) { return swap16(encodeUtf16LE(str, bom)); } function createTextDecoderUtf16BE() { try { const decoder = new TextDecoder("utf-16be"); return decoder; } catch { return { encoding: "utf-16be", fatal: false, ignoreBOM: false, decode: (input) => decoderUTF16LE.decode(swapBytes(input)) }; } } var UnsupportedEncodingError = class extends Error { constructor(encoding) { super(`Unsupported encoding: ${encoding}`); } }; function isGZipped(data) { if (typeof data === "string") return false; const buf = asUint8Array(data); return buf[0] === 31 && buf[1] === 139; } function decompressBuffer(data) { if (!isGZipped(data)) return data; const buf = arrayBufferViewToBuffer(data); return gunzipSync(buf); } //#endregion //#region src/common/CFileResource.ts var CFileResource = class CFileResource { _text; baseFilename; _gz; constructor(url, content, encoding, baseFilename, gz) { this.url = url; this.content = content; this.encoding = encoding; this.baseFilename = baseFilename ?? (url.protocol !== "data:" && url.pathname.split("/").pop() || void 0); this._gz = gz; } get gz() { if (this._gz !== void 0) return this._gz; if (this.url.pathname.endsWith(".gz")) return true; if (typeof this.content === "string") return false; return isGZipped(this.content); } getText(encoding) { if (this._text !== void 0) return this._text; const text = typeof this.content === "string" ? this.content : decode(this.content, encoding ?? this.encoding); this._text = text; return text; } getBytes() { const arrayBufferview = typeof this.content === "string" ? encodeString(this.content, this.encoding) : this.content; return arrayBufferview instanceof Uint8Array ? arrayBufferview : new Uint8Array(arrayBufferview.buffer, arrayBufferview.byteOffset, arrayBufferview.byteLength); } toJson() { return { url: this.url.href, content: this.getText(), encoding: this.encoding, baseFilename: this.baseFilename, gz: this.gz }; } static isCFileResource(obj) { return obj instanceof CFileResource; } static from(urlOrFileResource, content, encoding, baseFilename, gz) { if (CFileResource.isCFileResource(urlOrFileResource)) { if (content) { const { url, encoding: encoding$1, baseFilename: baseFilename$1, gz: gz$1 } = urlOrFileResource; return new CFileResource(url, content, encoding$1, baseFilename$1, gz$1); } return urlOrFileResource; } if (urlOrFileResource instanceof URL) { assert$1(content !== void 0); return new CFileResource(urlOrFileResource, content, encoding, baseFilename, gz); } if (content !== void 0) { const fileRef = urlOrFileResource; return new CFileResource(fileRef.url, content, fileRef.encoding, fileRef.baseFilename, fileRef.gz); } assert$1("content" in urlOrFileResource && urlOrFileResource.content !== void 0); const fileResource = urlOrFileResource; return new CFileResource(fileResource.url, fileResource.content, fileResource.encoding, fileResource.baseFilename, fileResource.gz); } }; function fromFileResource(fileResource, encoding) { return CFileResource.from(encoding ? { ...fileResource, encoding } : fileResource); } function renameFileResource(fileResource, url) { return CFileResource.from({ ...fileResource, url }); } //#endregion //#region src/common/stat.ts /** * Compare two Stats to see if they have the same value. * @param left - Stats * @param right - Stats * @returns 0 - equal; 1 - left > right; -1 left < right */ function compareStats(left, right) { if (left === right) return 0; if (left.eTag || right.eTag) return left.eTag === right.eTag ? 0 : (left.eTag || "") < (right.eTag || "") ? -1 : 1; const diff = left.size - right.size || left.mtimeMs - right.mtimeMs; return diff < 0 ? -1 : diff > 0 ? 1 : 0; } //#endregion //#region src/common/urlOrReferenceToUrl.ts function urlOrReferenceToUrl(urlOrReference) { return urlOrReference instanceof URL ? urlOrReference : urlOrReference.url; } //#endregion //#region src/CSpellIO.ts function toReadFileOptions(options) { if (!options) return options; if (typeof options === "string") return { encoding: options }; return options; } //#endregion //#region src/errors/error.ts function toError(e) { if (e instanceof Error) return e; if (typeof e === "object" && e && "message" in e && typeof e.message === "string") return new Error(e.message, { cause: e }); return new Error(e && e.toString()); } //#endregion //#region src/models/Stats.ts let FileType = /* @__PURE__ */ function(FileType$1) { /** * The file type is unknown. */ FileType$1[FileType$1["Unknown"] = 0] = "Unknown"; /** * A regular file. */ FileType$1[FileType$1["File"] = 1] = "File"; /** * A directory. */ FileType$1[FileType$1["Directory"] = 2] = "Directory"; /** * A symbolic link. */ FileType$1[FileType$1["SymbolicLink"] = 64] = "SymbolicLink"; return FileType$1; }({}); //#endregion //#region src/node/dataUrl.ts /** * Generates a string of the following format: * * `data:[mediaType][;charset=<encoding>[;base64],<data>` * * - `encoding` - defaults to `utf8` for text data * @param data * @param mediaType - The mediaType is a [MIME](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) type string * @param attributes - Additional attributes */ function encodeDataUrl(data, mediaType, attributes) { if (typeof data === "string") return encodeString$1(data, mediaType, attributes); const attribs = encodeAttributes(attributes || []); const buf = arrayBufferViewToBuffer(data); return `data:${mediaType}${attribs};base64,${buf.toString("base64url")}`; } function toDataUrl(data, mediaType, attributes) { return new URL(encodeDataUrl(data, mediaType, attributes)); } function encodeString$1(data, mediaType, attributes) { mediaType = mediaType || "text/plain"; attributes = attributes || []; const asUrlComp = encodeURIComponent(data); const asBase64 = Buffer.from(data).toString("base64url"); const useBase64 = asBase64.length < asUrlComp.length - 7; const encoded = useBase64 ? asBase64 : asUrlComp; const attribMap = new Map([["charset", "utf-8"], ...attributes]); attribMap.set("charset", "utf-8"); const attribs = encodeAttributes(attribMap); return `data:${mediaType}${attribs}${useBase64 ? ";base64" : ""},${encoded}`; } function encodeAttributes(attributes) { return [...attributes].map(([key, value]) => `;${key}=${encodeURIComponent(value)}`).join(""); } const dataUrlRegExHead = /^data:(?<mediaType>[^;,]*)(?<attributes>(?:;[^=]+=[^;,]*)*)(?<base64>;base64)?$/; function decodeDataUrl(url) { url = url.toString(); const [head, encodedData] = url.split(",", 2); if (!head || encodedData === void 0) throw new Error("Not a data url"); const match = head.match(dataUrlRegExHead); if (!match || !match.groups) throw new Error("Not a data url"); const mediaType = match.groups["mediaType"] || ""; const rawAttributes = (match.groups["attributes"] || "").split(";").filter((a) => !!a).map((entry) => entry.split("=", 2)).map(([key, value]) => [key, decodeURIComponent(value)]); const attributes = new Map(rawAttributes); const encoding = attributes.get("charset"); const isBase64 = !!match.groups["base64"]; const data = isBase64 ? Buffer.from(encodedData, "base64url") : Buffer.from(decodeURIComponent(encodedData)); return { mediaType, data, encoding, attributes }; } function guessMimeType(filename) { if (filename.endsWith(".trie")) return { mimeType: "application/vnd.cspell.dictionary+trie", encoding: "utf-8" }; if (filename.endsWith(".trie.gz")) return { mimeType: "application/vnd.cspell.dictionary+trie.gz" }; if (filename.endsWith(".txt")) return { mimeType: "text/plain", encoding: "utf-8" }; if (filename.endsWith(".txt.gz")) return { mimeType: "application/gzip" }; if (filename.endsWith(".gz")) return { mimeType: "application/gzip" }; if (filename.endsWith(".json")) return { mimeType: "application/json", encoding: "utf-8" }; if (filename.endsWith(".yaml") || filename.endsWith(".yml")) return { mimeType: "application/x-yaml", encoding: "utf-8" }; return void 0; } //#endregion //#region src/node/file/_fetch.ts /** alias of global.fetch, useful for mocking */ const _fetch = global.fetch; //#endregion //#region src/node/file/FetchError.ts var FetchUrlError = class FetchUrlError extends Error { constructor(message, code, status, url) { super(message); this.code = code; this.status = status; this.url = url; this.name = "FetchUrlError"; } static create(url, status, message) { if (status === 404) return new FetchUrlError(message || "URL not found.", "ENOENT", status, url); if (status >= 400 && status < 500) return new FetchUrlError(message || "Permission denied.", "EACCES", status, url); return new FetchUrlError(message || "Fatal Error", "ECONNREFUSED", status, url); } static fromError(url, e) { const cause = getCause(e); if (cause) return new FetchUrlError(cause.message, cause.code, void 0, url); if (isNodeError(e)) return new FetchUrlError(e.message, e.code, void 0, url); return new FetchUrlError(e.message, void 0, void 0, url); } }; function isNodeError(e) { if (e instanceof Error && "code" in e && typeof e.code === "string") return true; if (e && typeof e === "object" && "code" in e && typeof e.code === "string") return true; return false; } function isError(e) { return e instanceof Error; } function isErrorWithOptionalCause(e) { return isError(e) && (!("cause" in e) || isNodeError(e.cause) || isNodeError(e)); } function getCause(e) { return isErrorWithOptionalCause(e) ? e.cause : void 0; } function toFetchUrlError(err, url) { return err instanceof FetchUrlError ? err : FetchUrlError.fromError(url, toError$1(err)); } function toError$1(err) { return err instanceof Error ? err : new Error("Unknown Error", { cause: err }); } //#endregion //#region src/node/file/fetch.ts async function fetchHead(request) { const url = toURL$1(request); try { const r = await _fetch(url, { method: "HEAD" }); if (!r.ok) throw FetchUrlError.create(url, r.status); return r.headers; } catch (e) { throw toFetchUrlError(e, url); } } async function fetchURL(url, signal) { try { const request = signal ? new Request(url, { signal }) : url; const response = await _fetch(request); if (!response.ok) throw FetchUrlError.create(url, response.status); return Buffer.from(await response.arrayBuffer()); } catch (e) { throw toFetchUrlError(e, url); } } function toURL$1(url) { return typeof url === "string" ? new URL(url) : url; } //#endregion //#region src/node/file/stat.ts async function getStatHttp(url) { const headers = await fetchHead(url); const eTag = headers.get("etag") || void 0; const guessSize = Number.parseInt(headers.get("content-length") || "0", 10); return { size: eTag ? -1 : guessSize, mtimeMs: 0, eTag }; } //#endregion //#region src/requests/RequestFsReadFile.ts const RequestType$4 = "fs:readFile"; const RequestFsReadFile = requestFactory(RequestType$4); //#endregion //#region src/requests/RequestFsReadFileSync.ts const RequestType$3 = "fs:readFileSync"; const RequestFsReadFileTextSync = requestFactory(RequestType$3); //#endregion //#region src/requests/RequestFsStat.ts const RequestTypeStat = "fs:stat"; const RequestFsStat = requestFactory(RequestTypeStat); const RequestTypeStatSync = "fs:statSync"; const RequestFsStatSync = requestFactory(RequestTypeStatSync); //#endregion //#region src/requests/RequestFsWriteFile.ts const RequestType$2 = "fs:writeFile"; const RequestFsWriteFile = requestFactory(RequestType$2); //#endregion //#region src/requests/RequestZlibInflate.ts const RequestType$1 = "zlib:inflate"; const RequestZlibInflate = requestFactory(RequestType$1); //#endregion //#region src/requests/RequestFsReadDirectory.ts const RequestType = "fs:readDir"; const RequestFsReadDirectory = requestFactory(RequestType); //#endregion //#region src/handlers/node/file.ts const isGzFileRegExp = /\.gz($|[?#])/; function isGzFile(url) { return isGzFileRegExp.test(typeof url === "string" ? url : url.pathname); } const pGzip = promisify(gzip); /** * Handle Binary File Reads */ const handleRequestFsReadFile = RequestFsReadFile.createRequestHandler(({ params }) => { const baseFilename = urlBasename(params.url); return createResponse(promises.readFile(fileURLToPath(params.url)).then((content) => CFileResource.from(params.url, content, params.encoding, baseFilename))); }, void 0, "Node: Read Binary File."); /** * Handle Binary File Sync Reads */ const handleRequestFsReadFileSync = RequestFsReadFileTextSync.createRequestHandler(({ params }) => createResponse(CFileResource.from({ ...params, content: readFileSync(fileURLToPath(params.url)) })), void 0, "Node: Sync Read Binary File."); /** * Handle Binary File Reads */ const handleRequestFsReadDirectory = RequestFsReadDirectory.createRequestHandler(({ params }) => { return createResponse(promises.readdir(fileURLToPath(params.url), { withFileTypes: true }).then((entries) => direntToDirEntries(params.url, entries))); }, void 0, "Node: Read Directory."); /** * Handle deflating gzip data */ const handleRequestZlibInflate = RequestZlibInflate.createRequestHandler(({ params }) => createResponse(gunzipSync(arrayBufferViewToBuffer(params.data))), void 0, "Node: gz deflate."); const supportedFetchProtocols = { "http:": true, "https:": true }; /** * Handle fetching a file from http */ const handleRequestFsReadFileHttp = RequestFsReadFile.createRequestHandler((req, next) => { const { url, signal, encoding } = req.params; if (!(url.protocol in supportedFetchProtocols)) return next(req); return createResponse(fetchURL(url, signal).then((content) => CFileResource.from({ url, encoding, content }))); }, void 0, "Node: Read Http(s) file."); /** * Handle decoding a data url */ const handleRequestFsReadFileSyncData = RequestFsReadFileTextSync.createRequestHandler((req, next) => { const { url, encoding } = req.params; if (url.protocol !== "data:") return next(req); const data = decodeDataUrl(url); return createResponse(CFileResource.from({ url, content: data.data, encoding, baseFilename: data.attributes.get("filename") })); }, void 0, "Node: Read data: urls."); /** * Handle decoding a data url */ const handleRequestFsReadFileData = RequestFsReadFile.createRequestHandler((req, next, dispatcher) => { const { url } = req.params; if (url.protocol !== "data:") return next(req); const res = dispatcher.dispatch(RequestFsReadFileTextSync.create(req.params)); if (!isServiceResponseSuccess(res)) return res; return createResponse(Promise.resolve(res.value)); }, void 0, "Node: Read data: urls."); /** * Handle fs:stat */ const handleRequestFsStat = RequestFsStat.createRequestHandler(({ params }) => createResponse(toPromiseStats(promises.stat(fileURLToPath(params.url)))), void 0, "Node: fs.stat."); function toStats(stat) { return { size: stat.size, mtimeMs: stat.mtimeMs, fileType: toFileType(stat) }; } function toPromiseStats(pStat) { return pStat.then(toStats); } /** * Handle fs:statSync */ const handleRequestFsStatSync = RequestFsStatSync.createRequestHandler((req) => { const { params } = req; try { return createResponse(statSync(fileURLToPath(params.url))); } catch (e) { return createResponseFail(req, toError(e)); } }, void 0, "Node: fs.stat."); /** * Handle deflating gzip data */ const handleRequestFsStatHttp = RequestFsStat.createRequestHandler((req, next) => { const { url } = req.params; if (!(url.protocol in supportedFetchProtocols)) return next(req); return createResponse(getStatHttp(url)); }, void 0, "Node: http get stat"); /** * Handle fs:writeFile */ const handleRequestFsWriteFile = RequestFsWriteFile.createRequestHandler(({ params }) => createResponse(writeFile(params, params.content)), void 0, "Node: fs.writeFile"); async function writeFile(fileRef, content) { const gz = isGZipped(content); const { url, encoding, baseFilename } = fileRef; const resultRef = { url, encoding, baseFilename, gz }; await promises.writeFile(fileURLToPath(fileRef.url), encodeContent(fileRef, content)); return resultRef; } /** * Handle fs:writeFile */ const handleRequestFsWriteFileDataUrl = RequestFsWriteFile.createRequestHandler((req, next) => { const fileResource = req.params; const { url } = req.params; if (url.protocol !== "data:") return next(req); const gz = isGZipped(fileResource.content); const baseFilename = fileResource.baseFilename || "file.txt" + (gz ? ".gz" : ""); const mt = guessMimeType(baseFilename); const mediaType = mt?.mimeType || "text/plain"; const dataUrl = toDataUrl(fileResource.content, mediaType, [["filename", baseFilename]]); return createResponse(Promise.resolve({ url: dataUrl, baseFilename, gz, encoding: mt?.encoding })); }, void 0, "Node: fs.writeFile DataUrl"); /** * Handle fs:writeFile compressed */ const handleRequestFsWriteFileGz = RequestFsWriteFile.createRequestHandler((req, next, dispatcher) => { const fileResource = req.params; if (!fileResource.gz && !isGzFile(fileResource.url) && (!fileResource.baseFilename || !isGzFile(fileResource.baseFilename))) return next(req); if (typeof fileResource.content !== "string" && isGZipped(fileResource.content)) return next(req); return createResponse(compressAndChainWriteRequest(dispatcher, fileResource, fileResource.content)); }, void 0, "Node: fs.writeFile compressed"); async function compressAndChainWriteRequest(dispatcher, fileRef, content) { const buf = await pGzip(encodeContent(fileRef, content)); const res = dispatcher.dispatch(RequestFsWriteFile.create({ ...fileRef, content: buf })); assert$1(isServiceResponseSuccess(res)); return res.value; } function registerHandlers(serviceBus) { /** * Handlers are in order of low to high level * Order is VERY important. */ const handlers = [ handleRequestFsReadFile, handleRequestFsReadFileSync, handleRequestFsWriteFile, handleRequestFsWriteFileDataUrl, handleRequestFsWriteFileGz, handleRequestFsReadFileHttp, handleRequestFsReadFileData, handleRequestFsReadFileSyncData, handleRequestFsReadDirectory, handleRequestZlibInflate, handleRequestFsStatSync, handleRequestFsStat, handleRequestFsStatHttp ]; handlers.forEach((handler) => serviceBus.addHandler(handler)); } function encodeContent(ref, content) { if (typeof content === "string") { if ([ void 0, "utf8", "utf-8" ].includes(ref.encoding)) return content; return arrayBufferViewToBuffer(encodeString(content, ref.encoding)); } return arrayBufferViewToBuffer(content); } function mapperDirentToDirEntry(dir) { return (dirent) => direntToDirEntry(dir, dirent); } function direntToDirEntries(dir, dirent) { return dirent.map(mapperDirentToDirEntry(dir)); } function direntToDirEntry(dir, dirent) { return { name: dirent.name, dir, fileType: toFileType(dirent) }; } function toFileType(statLike) { const t = statLike.isFile() ? FileType.File : statLike.isDirectory() ? FileType.Directory : FileType.Unknown; return statLike.isSymbolicLink() ? t | FileType.SymbolicLink : t; } //#endregion //#region src/CSpellIONode.ts let defaultCSpellIONode = void 0; var CSpellIONode = class { constructor(serviceBus = new ServiceBus()) { this.serviceBus = serviceBus; registerHandlers(serviceBus); } readFile(urlOrFilename, options) { const readOptions = toReadFileOptions(options); const ref = toFileResourceRequest(urlOrFilename, readOptions?.encoding, readOptions?.signal); const res = this.serviceBus.dispatch(RequestFsReadFile.create(ref)); if (!isServiceResponseSuccess(res)) throw genError(res.error, "readFile"); return res.value; } readDirectory(urlOrFilename) { const ref = toFileReference(urlOrFilename); const res = this.serviceBus.dispatch(RequestFsReadDirectory.create(ref)); if (!isServiceResponseSuccess(res)) throw genError(res.error, "readDirectory"); return res.value; } readFileSync(urlOrFilename, encoding) { const ref = toFileReference(urlOrFilename, encoding); const res = this.serviceBus.dispatch(RequestFsReadFileTextSync.create(ref)); if (!isServiceResponseSuccess(res)) throw genError(res.error, "readFileSync"); return res.value; } writeFile(urlOrFilename, content) { const ref = toFileReference(urlOrFilename); const fileResource = CFileResource.from(ref, content); const res = this.serviceBus.dispatch(RequestFsWriteFile.create(fileResource)); if (!isServiceResponseSuccess(res)) throw genError(res.error, "writeFile"); return res.value; } getStat(urlOrFilename) { const ref = toFileReference(urlOrFilename); const res = this.serviceBus.dispatch(RequestFsStat.create(ref)); if (!isServiceResponseSuccess(res)) throw genError(res.error, "getStat"); return res.value; } getStatSync(urlOrFilename) { const ref = toFileReference(urlOrFilename); const res = this.serviceBus.dispatch(RequestFsStatSync.create(ref)); if (!isServiceResponseSuccess(res)) throw genError(res.error, "getStatSync"); return res.value; } compareStats(left, right) { return compareStats(left, right); } toURL(urlOrFilename, relativeTo) { if (isFileReference(urlOrFilename)) return urlOrFilename.url; return toURL(urlOrFilename, relativeTo); } toFileURL(urlOrFilename, relativeTo) { if (isFileReference(urlOrFilename)) return urlOrFilename.url; return toFileURL(urlOrFilename, relativeTo); } urlBasename(urlOrFilename) { return urlBasename(this.toURL(urlOrFilename)); } urlDirname(urlOrFilename) { return urlDirname(this.toURL(urlOrFilename)); } }; function genError(err, alt) { return err || new ErrorNotImplemented(alt); } function getDefaultCSpellIO() { if (defaultCSpellIONode) return defaultCSpellIONode; const cspellIO = new CSpellIONode(); defaultCSpellIONode = cspellIO; return cspellIO; } //#endregion //#region src/VirtualFS.ts const debug = false; //#endregion //#region src/VirtualFS/findUpFromUrl.ts async function findUpFromUrl(name, from, options) { const { type: entryType = "file", stopAt, fs: fs$1 } = options; let dir = new URL(".", from); const root = new URL("/", dir); const predicate = makePredicate(fs$1, name, entryType); const stopAtHrefs = new Set((Array.isArray(stopAt) ? stopAt : [stopAt || root]).map((p) => new URL(".", p).href)); let last = ""; while (dir.href !== last) { const found = await predicate(dir); if (found !== void 0) return found; last = dir.href; if (dir.href === root.href || stopAtHrefs.has(dir.href)) break; dir = new URL("..", dir); } return void 0; } function makePredicate(fs$1, name, entryType) { if (typeof name === "function") return name; const checkStat = entryType === "file" || entryType === "!file" ? "isFile" : "isDirectory"; const checkValue = entryType.startsWith("!") ? false : true; function checkName(dir, name$1) { const f = new URL(name$1, dir); return fs$1.stat(f).then((stats) => (stats.isUnknown() || stats[checkStat]() === checkValue) && f || void 0).catch(() => void 0); } if (!Array.isArray(name)) return (dir) => checkName(dir, name); return async (dir) => { const pending = name.map((n) => checkName(dir, n)); for (const p of pending) { const found = await p; if (found) return found; } return void 0; }; } //#endregion //#region src/VirtualFS/CVFileSystem.ts var CVFileSystem = class { #core; readFile; writeFile; stat; readDirectory; getCapabilities; constructor(core) { this.#core = core; this.readFile = this.#core.readFile.bind(this.#core); this.writeFile = this.#core.writeFile.bind(this.#core); this.stat = this.#core.stat.bind(this.#core); this.readDirectory = this.#core.readDirectory.bind(this.#core); this.getCapabilities = this.#core.getCapabilities.bind(this.#core); } get providerInfo() { return this.#core.providerInfo; } get hasProvider() { return this.#core.hasProvider; } findUp(name, from, options = {}) { const opts = { ...options, fs: this.#core }; return findUpFromUrl(name, from, opts); } }; //#endregion //#region src/VFileSystem.ts let FSCapabilityFlags = /* @__PURE__ */ function(FSCapabilityFlags$1) { FSCapabilityFlags$1[FSCapabilityFlags$1["None"] = 0] = "None"; FSCapabilityFlags$1[FSCapabilityFlags$1["Stat"] = 1] = "Stat"; FSCapabilityFlags$1[FSCapabilityFlags$1["Read"] = 2] = "Read"; FSCapabilityFlags$1[FSCapabilityFlags$1["Write"] = 4] = "Write"; FSCapabilityFlags$1[FSCapabilityFlags$1["ReadWrite"] = 6] = "ReadWrite"; FSCapabilityFlags$1[FSCapabilityFlags$1["ReadDir"] = 8] = "ReadDir"; FSCapabilityFlags$1[FSCapabilityFlags$1["WriteDir"] = 16] = "WriteDir"; FSCapabilityFlags$1[FSCapabilityFlags$1["ReadWriteDir"] = 24] = "ReadWriteDir"; return FSCapabilityFlags$1; }({}); //#endregion //#region src/VirtualFS/WrappedProviderFs.ts function cspellIOToFsProvider(cspellIO) { const capabilities = FSCapabilityFlags.Stat | FSCapabilityFlags.ReadWrite | FSCapabilityFlags.ReadDir; const capabilitiesHttp = capabilities & ~FSCapabilityFlags.Write & ~FSCapabilityFlags.ReadDir; const capMap = { "file:": capabilities, "http:": capabilitiesHttp, "https:": capabilitiesHttp }; const name = "CSpellIO"; const supportedProtocols = new Set([ "file:", "http:", "https:" ]); const fs$1 = { providerInfo: { name }, stat: (url) => cspellIO.getStat(url), readFile: (url, options) => cspellIO.readFile(url, options), readDirectory: (url) => cspellIO.readDirectory(url), writeFile: (file) => cspellIO.writeFile(file.url, file.content), dispose: () => void 0, capabilities, getCapabilities(url) { return fsCapabilities(capMap[url.protocol] || FSCapabilityFlags.None); } }; return { name, getFileSystem: (url, _next) => { return supportedProtocols.has(url.protocol) ? fs$1 : void 0; } }; } function wrapError(e) { if (e instanceof VFSError) return e; return e; } var VFSError = class extends Error { constructor(message, options) { super(message, options); } }; var VFSErrorUnsupportedRequest = class extends VFSError { url; constructor(request, url, parameters) { super(`Unsupported request: ${request}`); this.request = request; this.parameters = parameters; this.url = url?.toString(); } }; var CFsCapabilities = class { constructor(flags) { this.flags = flags; } get readFile() { return !!(this.flags & FSCapabilityFlags.Read); } get writeFile() { return !!(this.flags & FSCapabilityFlags.Write); } get readDirectory() { return !!(this.flags & FSCapabilityFlags.ReadDir); } get writeDirectory() { return !!(this.flags & FSCapabilityFlags.WriteDir); } get stat() { return !!(this.flags & FSCapabilityFlags.Stat); } }; function fsCapabilities(flags) { return new CFsCapabilities(flags); } var WrappedProviderFs = class WrappedProviderFs { hasProvider; capabilities; providerInfo; _capabilities; constructor(fs$1, eventLogger) { this.fs = fs$1; this.eventLogger = eventLogger; this.hasProvider = !!fs$1; this.capabilities = fs$1?.capabilities || FSCapabilityFlags.None; this._capabilities = fsCapabilities(this.capabilities); this.providerInfo = fs$1?.providerInfo || { name: "unknown" }; } logEvent(method, event, traceID, url, message) { this.eventLogger({ method, event, url, traceID, ts: performance.now(), message }); } getCapabilities(url) { if (this.fs?.getCapabilities) return this.fs.getCapabilities(url); return this._capabilities; } async stat(urlRef) { const traceID = performance.now(); const url = urlOrReferenceToUrl(urlRef); this.logEvent("stat", "start", traceID, url); try { checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.Stat, "stat", url); return new CVfsStat(await this.fs.stat(urlRef)); } catch (e) { this.logEvent("stat", "error", traceID, url, e instanceof Error ? e.message : ""); throw wrapError(e); } finally { this.logEvent("stat", "end", traceID, url); } } async readFile(urlRef, optionsOrEncoding) { const traceID = performance.now(); const url = urlOrReferenceToUrl(urlRef); this.logEvent("readFile", "start", traceID, url); try { checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.Read, "readFile", url); const readOptions = toOptions(optionsOrEncoding); return fromFileResource(await this.fs.readFile(urlRef, readOptions), readOptions?.encoding); } catch (e) { this.logEvent("readFile", "error", traceID, url, e instanceof Error ? e.message : ""); throw wrapError(e); } finally { this.logEvent("readFile", "end", traceID, url); } } async readDirectory(url) { const traceID = performance.now(); this.logEvent("readDir", "start", traceID, url); try { checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.ReadDir, "readDirectory", url); return (await this.fs.readDirectory(url)).map((e) => new CVfsDirEntry(e)); } catch (e) { this.logEvent("readDir", "error", traceID, url, e instanceof Error ? e.message : ""); throw wrapError(e); } finally { this.logEvent("readDir", "end", traceID, url); } } async writeFile(file) { const traceID = performance.now(); const url = file.url; this.logEvent("writeFile", "start", traceID, url); try { checkCapabilityOrThrow(this.fs, this.capabilities, FSCapabilityFlags.Write, "writeFile", file.url); return await this.fs.writeFile(file); } catch (e) { this.logEvent("writeFile", "error", traceID, url, e instanceof Error ? e.message : ""); throw wrapError(e); } finally { this.logEvent("writeFile", "end", traceID, url); } } static disposeOf(fs$1) { fs$1 instanceof WrappedProviderFs && fs$1.fs?.dispose(); } }; function checkCapabilityOrThrow(fs$1, capabilities, flag, name, url) { if (!(capabilities & flag)) throw new VFSErrorUnsupportedRequest(name, url); } var CFileType = class { constructor(fileType) { this.fileType = fileType; } isFile() { return this.fileType === FileType.File; } isDirectory() { return this.fileType === FileType.Directory; } isUnknown() { return !this.fileType; } isSymbolicLink() { return !!(this.fileType & FileType.SymbolicLink); } }; var CVfsStat = class extends CFileType { constructor(stat) { super(stat.fileType || FileType.Unknown); this.stat = stat; } get size() { return this.stat.size; } get mtimeMs() { return this.stat.mtimeMs; } get eTag() { return this.stat.eTag; } }; var CVfsDirEntry = class extends CFileType { _url; constructor(entry) { super(entry.fileType); this.entry = entry; } get name() { return this.entry.name; } get dir() { return this.entry.dir; } get url() { if (this._url) return this._url; this._url = new URL(this.entry.name, this.entry.dir); return this._url; } toJSON() { return { name: this.name, dir: this.dir, fileType: this.fileType }; } }; function chopUrl(url) { if (!url) return ""; const href = url.href; const parts = href.split("/"); const n = parts.indexOf("node_modules"); if (n > 0) { const tail = parts.slice(Math.max(parts.length - 3, n + 1)); return parts.slice(0, n + 1).join("/") + "/…/" + tail.join("/"); } return href; } function rPad(str, len, ch = " ") { return str.padEnd(len, ch); } function toOptions(val) { return typeof val === "string" ? { encoding: val } : val; } //#endregion //#region src/CVirtualFS.ts var CVirtualFS = class { providers = /* @__PURE__ */ new Set(); cachedFs = /* @__PURE__ */ new Map(); revCacheFs = /* @__PURE__ */ new Map(); fsc; fs; loggingEnabled = debug; constructor() { this.fsc = fsPassThroughCore((url) => this._getFS(url)); this.fs = new CVFileSystem(this.fsc); } enableLogging(value) { this.loggingEnabled = value ?? true; } log = console.log; logEvent = (event) => { if (this.loggingEnabled) { const id = event.traceID.toFixed(13).replaceAll(/\d{4}(?=\d)/g, "$&."); const msg = event.message ? `\n\t\t${event.message}` : ""; const method = rPad(`${event.method}-${event.event}`, 16); this.log(`${method} ID:${id} ts:${event.ts.toFixed(13)} ${chopUrl(event.url)}${msg}`); } }; registerFileSystemProvider(...providers) { providers.forEach((provider) => this.providers.add(provider)); this.reset(); return { dispose: () => { for (const provider of providers) { for (const key of this.revCacheFs.get(provider) || []) this.cachedFs.delete(key); this.providers.delete(provider); } this.reset(); } }; } getFS(url) { return new CVFileSystem(this._getFS(url)); } _getFS(url) { const key = `${url.protocol}${url.hostname}`; const cached = this.cachedFs.get(key); if (cached) return cached; const fnNext = (provider, next$1) => { return (url$1) => { let calledNext = false; const fs$2 = provider.getFileSystem(url$1, (_url) => { calledNext = calledNext || url$1 === _url; return next$1(_url); }); if (fs$2) { const s = this.revCacheFs.get(provider) || /* @__PURE__ */ new Set(); s.add(key); this.revCacheFs.set(provider, s); return fs$2; } if (!calledNext) return next$1(url$1); return void 0; }; }; let next = (_url) => void 0; for (const provider of this.providers) next = fnNext(provider, next); const fs$1 = new WrappedProviderFs(next(url), this.logEvent); this.cachedFs.set(key, fs$1); return fs$1; } reset() { this.disposeOfCachedFs(); } disposeOfCachedFs() { for (const [key, fs$1] of [...this.cachedFs].reverse()) { try { WrappedProviderFs.disposeOf(fs$1); } catch {} this.cachedFs.delete(key); } this.cachedFs.clear(); this.revCacheFs.clear(); } dispose() { this.disposeOfCachedFs(); const providers = [...this.providers].reverse(); for (const provider of providers) try { provider.dispose?.(); } catch {} } }; function fsPassThroughCore(fs$1) { function gfs(ur, name) { const url = urlOrReferenceToUrl(ur); const f = fs$1(url); if (!f.hasProvider) throw new VFSErrorUnsupportedRequest(name, url, ur instanceof URL ? void 0 : { url: ur.url.toString(), encoding: ur.encoding }); return f; } return { providerInfo: { name: "default" }, hasProvider: true, stat: async (url) => gfs(url, "stat").stat(url), readFile: async (url, options) => gfs(url, "readFile").readFile(url, options), writeFile: async (file) => gfs(file, "writeFile").writeFile(file), readDirectory: async (url) => gfs(url, "readDirectory").readDirectory(url).then((entries) => entries.map((e) => new CVfsDirEntry(e))), getCapabilities: (url) => gfs(url, "getCapabilities").getCapabilities(url) }; } function createVirtualFS(cspellIO) { const cspell = cspellIO || getDefaultCSpellIO(); const vfs = new CVirtualFS(); vfs.registerFileSystemProvider(cspellIOToFsProvider(cspell)); return vfs; } let defaultVirtualFs = void 0; function getDefaultVirtualFs() { if (!defaultVirtualFs) defaultVirtualFs = createVirtualFS(); return defaultVirtualFs; } //#endregion //#region src/common/transformers.ts function encoderTransformer(iterable, encoding) { return isAsyncIterable(iterable) ? encoderAsyncIterable(iterable, encoding) : encoderIterable(iterable, encoding); } function* encoderIterable(iterable, encoding) { let useBom = true; for (const chunk of iterable) { yield encodeString(chunk, encoding, useBom); useBom = false; } } async function* encoderAsyncIterable(iterable, encoding) { let useBom = true; for await (const chunk of iterable) { yield encodeString(chunk, encoding, useBom); useBom = false; } } function isAsyncIterable(v) { return v && typeof v === "object" && !!v[Symbol.asyncIterator]; } //#endregion //#region src/node/file/fileWriter.ts const pipeline = promisify(Stream.pipeline); function writeToFile(filename, data, encoding) { return writeToFileIterable(filename, typeof data === "string" ? [data] : data, encoding); } function writeToFileIterable(filename, data, encoding) { const stream = Stream.Readable.from(encoderTransformer(data, encoding)); const zip = /\.gz$/.test(filename) ? zlib.createGzip() : new Stream.PassThrough(); return pipeline(stream, zip, fs.createWriteStream(filename)); } //#endregion //#region src/file/file.ts async function readFileText(filename, encoding) { const fr = await getDefaultCSpellIO().readFile(filename, encoding); return fr.getText(); } function readFileTextSync(filename, encoding) { return getDefaultCSpellIO().readFileSync(filename, encoding).getText(); } async function getStat(filenameOrUri) { try { return await getDefaultCSpellIO().getStat(filenameOrUri); } catch (e) { return toError(e); } } function getStatSync(filenameOrUri) { try { return getDefaultCSpellIO().getStatSync(filenameOrUri); } catch (e) { return toError(e); } } //#endregion //#region src/VirtualFS/redirectProvider.ts var RedirectProvider = class { constructor(name, publicRoot, privateRoot, options = { capabilitiesMask: -1 }) { this.name = name; this.publicRoot = publicRoot; this.privateRoot = privateRoot; this.options = options; } getFileSystem(url, next) { if (url.protocol !== this.publicRoot.protocol || url.host !== this.publicRoot.host) return void 0; const privateFs = next(this.privateRoot); if (!privateFs) return void 0; const shadowFS = next(url); return remapFS(this.name, privateFs, shadowFS, this.publicRoot, this.privateRoot, this.options); } }; /** * Create a provider that will redirect requests from the publicRoot to the privateRoot. * This is useful for creating a virtual file system that is a subset of another file system. * * Example: * ```ts * const vfs = createVirtualFS(); * const provider = createRedirectProvider('test', new URL('file:///public/'), new URL('file:///private/')) * vfs.registerFileSystemProvider(provider); * // Read the content of `file:///private/file.txt` * const file = vfs.fs.readFile(new URL('file:///public/file.txt'); * ``` * * @param name - name of the provider * @param publicRoot - the root of the public file system. * @param privateRoot - the root of the private file system. * @param options - options for the provider. * @returns FileSystemProvider */ function createRedirectProvider(name, publicRoot, privateRoot, options) { assert(publicRoot.pathname.endsWith("/"), "publicRoot must end with a slash"); assert(privateRoot.pathname.endsWith("/"), "privateRoot must end with a slash"); return new RedirectProvider(name, publicRoot, privateRoot, options); } /** * Create a Remapped file system that will redirect requests from the publicRoot to the privateRoot. * Requests that do not match the publicRoot will be passed to the shadowFs. * @param name - name of the provider * @param fs - the private file system * @param shadowFs - the file system that is obscured by the redirect. * @param publicRoot - the root of the public file system. * @param privateRoot - the root of the private file system. * @returns ProviderFileSystem */ function remapFS(name, fs$1, shadowFs, publicRoot, privateRoot, options) { const { capabilitiesMask = -1, capabilities } = options; function mapToPrivate(url) { const relativePath = url.pathname.slice(publicRoot.pathname.length); return new URL(relativePath, privateRoot); } function mapToPublic(url) { const relativePath = url.pathname.slice(privateRoot.pathname.length); return new URL(relativePath, publicRoot); } const mapFileReferenceToPrivate = (ref) => { return renameFileReference(ref, mapToPrivate(ref.url)); }; const mapFileReferenceToPublic = (ref) => { return renameFileReference(ref, mapToPublic(ref.url)); }; const mapUrlOrReferenceToPrivate = (urlOrRef) => { return urlOrRef instanceof URL ? mapToPrivate(urlOrRef) : mapFileReferenceToPrivate(urlOrRef); }; const mapFileResourceToPublic = (res) => { return renameFileResource(res, mapToPublic(res.url)); }; const mapFileResourceToPrivate = (res) => { return renameFileResource(res, mapToPrivate(res.url)); }; const mapDirEntryToPublic = (de) => { const dir = mapToPublic(de.dir); return { ...de, dir }; }; const fs2 = { stat: async (url) => { const url2 = mapUrlOrReferenceToPrivate(url); const stat = await fs$1.stat(url2); return stat; }, readFile: async (url, options$1) => { const url2 = mapUrlOrReferenceToPrivate(url); const file = await fs$1.readFile(url2, options$1); return mapFileResourceToPublic(file); }, readDirectory: async (url) => { const url2 = mapToPrivate(url); const dir = await fs$1.readDirectory(url2); return dir.map(mapDirEntryToPublic); }, writeFile: async (file) => { const fileRef2 = mapFileResourceToPrivate(file); const fileRef3 = await fs$1.writeFile(fileRef2); return mapFileReferenceToPublic(fileRef3); }, providerInfo: { ...fs$1.providerInfo, name }, capabilities: capabilities ?? fs$1.capabilities & capabilitiesMask, dispose: () => fs$1.dispose() }; return fsPassThrough(fs2, shadowFs, publicRoot); } function fsPassThrough(fs$1, shadowFs, root) { function gfs(ur, name) { const url = urlOrReferenceToUrl(ur); const f = url.href.startsWith(root.href) ? fs$1 : shadowFs; if (!f) throw new VFSErrorUnsupportedRequest(name, url, ur instanceof URL ? void 0 : { url: ur.url.toString(), encoding: ur.encoding }); return f; } const passThroughFs = { get providerInfo() { return fs$1.providerInfo; }, get capabilities() { return fs$1.capabilities; }, stat: async (url) => gfs(url, "stat").stat(url), readFile: async (url) => gfs(url, "readFile").readFile(url), writeFile: async (file) => gfs(file, "writeFile").writeFile(file), readDirectory: async (url) => gfs(url, "readDirectory").readDirectory(url), getCapabilities(url) { const f = gfs(url, "getCapabilities"); return f.getCapabilities ? f.getCapabilities(url) : fsCapabilities(f.capabilities); }, dispose: () => { fs$1.dispose(); shadowFs?.dispose(); } }; return passThroughFs; } //#endregion export { CFileReference, CFileResource, CSpellIONode, FSCapabilityFlags, FileType as VFileType, toArray as asyncIterableToArray, compareStats, createRedirectProvider, fromFileResource as createTextFileResource, createVirtualFS, encodeDataUrl, getDefaultCSpellIO, getDefaultVirtualFs, getStat, getStatSync, isFileURL, isUrlLike, readFileText, readFileTextSync, renameFileReference, renameFileResource, toDataUrl, toFileURL, toURL, urlBasename, urlDirname, urlOrReferenceToUrl, writeToFile, writeToFileIterable, writeToFileIterable as writeToFileIterableP }; //# sourceMappingURL=index.js.map