UNPKG

@rushstack/heft

Version:

Build all your JavaScript projects the same way: A way that works.

208 lines 9.12 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import * as path from 'node:path'; import { Path } from '@rushstack/node-core-library'; /* eslint-enable @rushstack/no-new-null */ const IS_WINDOWS = process.platform === 'win32'; /** * A filesystem adapter for use with the "fast-glob" package. This adapter uses a static set of paths * to provide a virtual filesystem. * * @remarks This adapter only implements methods required to allow for globbing. This means that it * does not support returning true-to-disk file stats or dirent objects. Instead, the returned file * stats and dirent objects only implement the `isDirectory` and `isFile` methods, which are * required for filesystem traversal performed by the globber. */ export class StaticFileSystemAdapter { /** * Create a new StaticFileSystemAdapter instance with the provided file paths. */ constructor(filePaths) { this._directoryMap = new Map(); /** { @inheritdoc fs.lstat } */ this.lstat = ((filePath, callback) => { process.nextTick(() => { let result; try { result = this.lstatSync(filePath); } catch (e) { callback(e, {}); return; } callback(null, result); }); }); /** { @inheritdoc fs.lstatSync } */ this.lstatSync = ((filePath) => { filePath = this._normalizePath(filePath); const entry = this._directoryMap.get(filePath); if (!entry) { const error = new Error(`ENOENT: no such file or directory, stat '${filePath}'`); error.code = 'ENOENT'; error.syscall = 'stat'; error.errno = -4058; error.path = filePath; throw error; } // We should only need to implement these methods for the purposes of fast-glob return { isFile: () => !entry.children, isDirectory: () => !!entry.children, isBlockDevice: () => false, isCharacterDevice: () => false, isSymbolicLink: () => false, isFIFO: () => false, isSocket: () => false }; }); /** { @inheritdoc fs.stat } */ this.stat = ((filePath, callback) => { this.lstat(filePath, callback); }); /** { @inheritdoc fs.statSync } */ this.statSync = ((filePath) => { return this.lstatSync(filePath); }); /** { @inheritdoc fs.readdir } */ this.readdir = ((filePath, optionsOrCallback, callback) => { // Default to no options, which will return a string callback let options; if (typeof optionsOrCallback === 'object') { options = optionsOrCallback; } else if (typeof optionsOrCallback === 'function') { callback = optionsOrCallback; } // Perform the readdir on the next tick to avoid blocking the event loop process.nextTick(() => { let result; try { if (options === null || options === void 0 ? void 0 : options.withFileTypes) { result = this.readdirSync(filePath, options); } else { result = this.readdirSync(filePath); } } catch (e) { callback(e, []); return; } // When "withFileTypes" is false or undefined, the callback is expected to return a string array. // Otherwise, we return a fs.Dirent array. if (options === null || options === void 0 ? void 0 : options.withFileTypes) { callback(null, result); } else { callback(null, result); } }); }); /** { @inheritdoc fs.readdirSync } */ this.readdirSync = ((filePath, options) => { filePath = this._normalizePath(filePath); const virtualDirectory = this._directoryMap.get(filePath); if (!virtualDirectory) { // Immitate a missing directory read from fs.readdir const error = new Error(`ENOENT: no such file or directory, scandir '${filePath}'`); error.code = 'ENOENT'; error.syscall = 'scandir'; error.errno = -4058; error.path = filePath; throw error; } else if (!virtualDirectory.children) { // Immitate a directory read of a file from fs.readdir const error = new Error(`ENOTDIR: not a directory, scandir '${filePath}'`); error.code = 'ENOTDIR'; error.syscall = 'scandir'; error.errno = -4052; error.path = filePath; throw error; } // When "withFileTypes" is false or undefined, the method is expected to return a string array. // Otherwise, we return a fs.Dirent array. const result = Array.from(virtualDirectory.children); if (options === null || options === void 0 ? void 0 : options.withFileTypes) { return result.map((entry) => { // Partially implement the fs.Dirent interface, only including the properties used by fast-glob return { name: entry.name, isFile: () => !entry.children, isDirectory: () => !!entry.children, isBlockDevice: () => false, isCharacterDevice: () => false, isSymbolicLink: () => false, isFIFO: () => false, isSocket: () => false }; }); } else { return result.map((entry) => entry.name); } }); for (const filePath of filePaths || []) { this.addFile(filePath); } } /** * Add a file and it's parent directories to the static virtual filesystem. */ addFile(filePath) { filePath = this._normalizePath(filePath); const existingPath = this._directoryMap.get(filePath); if (!existingPath) { // Set an entry without children for the file. Entries with undefined children are assumed to be files. let childPath = filePath; let childEntry = { name: path.basename(childPath) }; this._directoryMap.set(childPath, childEntry); // Loop through the path segments and create entries for each directory, if they don't already exist. // If they do, append to the existing children set and continue. let parentPath; while ((parentPath = path.dirname(childPath)) !== childPath) { const existingParentEntry = this._directoryMap.get(parentPath); if (existingParentEntry) { // If there is already an existing parent entry, add the child entry to the existing children set // and exit early, since the parent entries already exist. existingParentEntry.children.add(childEntry); break; } else { // If there is no existing parent entry, create a new entry with the child entry as the only child. const parentEntry = { name: path.basename(parentPath), children: new Set([childEntry]) }; this._directoryMap.set(parentPath, parentEntry); childEntry = parentEntry; childPath = parentPath; } } } } /** * Remove a file from the static virtual filesystem. */ removeFile(filePath) { filePath = this._normalizePath(filePath); const existingEntry = this._directoryMap.get(filePath); if (existingEntry) { // Remove the entry from the map and the parent's children set this._directoryMap.delete(filePath); this._directoryMap.get(path.dirname(filePath)).children.delete(existingEntry); } } /** * Remove all files from the static virtual filesystem. */ removeAllFiles() { this._directoryMap.clear(); } _normalizePath(filePath) { // On Windows, normalize to backslashes so that errors have the correct path format return IS_WINDOWS ? Path.convertToBackslashes(filePath) : filePath; } } //# sourceMappingURL=StaticFileSystemAdapter.js.map