fs/lib/adapters/Memory.js

import {getLogger} from "@ui5/logger";
const log = getLogger("resources:adapters:Memory");
import micromatch from "micromatch";
import AbstractAdapter from "./AbstractAdapter.js";

const ADAPTER_NAME = "Memory";

/**
 * Virtual resource Adapter
 *
 * @public
 * @class
 * @alias @ui5/fs/adapters/Memory
 * @extends @ui5/fs/adapters/AbstractAdapter
 */
class Memory extends AbstractAdapter {
	/**
	 * The constructor.
	 *
	 * @public
	 * @param {object} parameters Parameters
	 * @param {string} parameters.virBasePath
	 *   Virtual base path. Must be absolute, POSIX-style, and must end with a slash
	 * @param {string[]} [parameters.excludes] List of glob patterns to exclude
	 * @param {@ui5/project/specifications/Project} [parameters.project] Project this adapter belongs to (if any)
	 */
	constructor({virBasePath, project, excludes}) {
		super({virBasePath, project, excludes});
		this._virFiles = Object.create(null); // map full of files
		this._virDirs = Object.create(null); // map full of directories
	}

	/**
	 * Matches and returns resources from a given map (either _virFiles or _virDirs).
	 *
	 * @private
	 * @param {string[]} patterns
	 * @param {object} resourceMap
	 * @returns {Promise<module:@ui5/fs.Resource[]>}
	 */
	async _matchPatterns(patterns, resourceMap) {
		const resourcePaths = Object.keys(resourceMap);
		const matchedPaths = micromatch(resourcePaths, patterns, {
			dot: true
		});
		return await Promise.all(matchedPaths.map((virPath) => {
			const resource = resourceMap[virPath];
			if (resource) {
				return this._cloneResource(resource);
			}
		}));
	}

	async _cloneResource(resource) {
		const clonedResource = await resource.clone();
		if (this._project) {
			clonedResource.setProject(this._project);
		}
		return clonedResource;
	}

	/**
	 * Locate resources by glob.
	 *
	 * @private
	 * @param {Array} patterns array of glob patterns
	 * @param {object} [options={}] glob options
	 * @param {boolean} [options.nodir=true] Do not match directories
	 * @param {@ui5/fs/tracing.Trace} trace Trace instance
	 * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources
	 */
	async _runGlob(patterns, options = {nodir: true}, trace) {
		if (patterns[0] === "" && !options.nodir) { // Match virtual root directory
			return [
				this._createResource({
					project: this._project,
					statInfo: { // TODO: make closer to fs stat info
						isDirectory: function() {
							return true;
						}
					},
					sourceMetadata: {
						adapter: ADAPTER_NAME
					},
					path: this._virBasePath.slice(0, -1)
				})
			];
		}

		let matchedResources = await this._matchPatterns(patterns, this._virFiles);

		if (!options.nodir) {
			const matchedDirs = await this._matchPatterns(patterns, this._virDirs);
			matchedResources = matchedResources.concat(matchedDirs);
		}

		return matchedResources;
	}

	/**
	 * Locates resources by path.
	 *
	 * @private
	 * @param {string} virPath Virtual path
	 * @param {object} options Options
	 * @param {@ui5/fs/tracing.Trace} trace Trace instance
	 * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource
	 */
	async _byPath(virPath, options, trace) {
		const relPath = this._resolveVirtualPathToBase(virPath);
		if (relPath === null) {
			return null;
		}

		trace.pathCall();

		const resource = this._virFiles[relPath];

		if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) {
			return null;
		} else {
			return await this._cloneResource(resource);
		}
	}

	/**
	 * Writes the content of a resource to a path.
	 *
	 * @private
	 * @param {@ui5/fs/Resource} resource The Resource to write
	 * @returns {Promise<undefined>} Promise resolving once data has been written
	 */
	async _write(resource) {
		resource = this._migrateResource(resource);
		if (resource instanceof Promise) {
			// Only await if the migrate function returned a promise
			// Otherwise await would automatically create a Promise, causing unwanted overhead
			resource = await resource;
		}
		this._assignProjectToResource(resource);
		const relPath = this._resolveVirtualPathToBase(resource.getPath(), true);
		log.silly(`Writing to virtual path ${resource.getPath()}`);
		this._virFiles[relPath] = await resource.clone();

		// Add virtual directories for all path segments of the written resource
		// TODO: Add tests for all this
		const pathSegments = relPath.split("/");
		pathSegments.pop(); // Remove last segment representing the resource itself

		pathSegments.forEach((segment, i) => {
			if (i >= 1) {
				segment = pathSegments[i - 1] + "/" + segment;
			}
			pathSegments[i] = segment;
		});

		for (let i = pathSegments.length - 1; i >= 0; i--) {
			const segment = pathSegments[i];
			if (!this._virDirs[segment]) {
				this._virDirs[segment] = this._createResource({
					project: this._project,
					sourceMetadata: {
						adapter: ADAPTER_NAME
					},
					statInfo: { // TODO: make closer to fs stat info
						isDirectory: function() {
							return true;
						}
					},
					path: this._virBasePath + segment
				});
			}
		}
	}
}

export default Memory;