const path = require('path');
const { providerCreator } = require('../shared/jimpleFns');
/**
* @module node/pathUtils
*/
/**
* @typedef {import('../shared/jimpleFns').ProviderCreator<O>} ProviderCreator
* @template O
*/
/**
* @typedef {Object} PathUtilsProviderOptions
* @property {string} serviceName The name that will be used to register an instance of
* {@link PathUtils}. Its default value is `pathUtils`.
* @property {string} [home] The path to the new home location.
* @parent module:node/pathUtils
*/
/**
* A utility services to manage paths on a project. It allows for path building relatives
* to the project root or from where the app executable is located.
*
* @parent module:node/pathUtils
* @tutorial pathUtils
*/
class PathUtils {
/**
* @param {string} [home=''] The location of the project's `home`(root) directory. By
* default it uses `process.cwd()`.
*/
constructor(home = '') {
/**
* The root path from where the app is being executed.
*
* @type {string}
* @access protected
* @ignore
*/
this._path = process.cwd();
/**
* A dictionary of different path locations.
*
* @type {Object.<string, string>}
* @access protected
* @ignore
*/
this._locations = {};
this._addAppLocation();
this.addLocation('home', home || this.path);
}
/**
* Adds a new location.
*
* @param {string} name The reference name.
* @param {string} locationPath The path of the location. It must be inside the path
* from the app is being executed.
*/
addLocation(name, locationPath) {
let location = locationPath;
/**
* If it doesn't starts with the root location, then prefix it with it. The project should
* never attempt to access a location outside its directory.
*/
if (location !== this.path && !location.startsWith(this.path)) {
location = path.join(this.path, location);
}
// Fix it so all the locations will end with a separator.
if (!location.endsWith(path.sep)) {
location = `${location}${path.sep}`;
}
// Add it to the dictionary.
this.locations[name] = location;
}
/**
* Gets a location path by its name.
*
* @param {string} name The location name.
* @returns {string}
* @throws {Error} If there location hasn't been added.
*/
getLocation(name) {
const location = this.locations[name];
if (!location) {
throw new Error(`There's no location with the following name: ${name}`);
}
return location;
}
/**
* Alias to `joinFrom` that uses the `home` location by default.
*
* @param {...string} paths The rest of the path components to join.
* @returns {string}
*/
join(...paths) {
return this.joinFrom('home', ...paths);
}
/**
* Builds a path using a location path as base.
*
* @param {string} location The location name.
* @param {...string} paths The rest of the path components to join.
* @returns {string}
*/
joinFrom(location, ...paths) {
const locationPath = this.getLocation(location);
return path.join(locationPath, ...paths);
}
/**
* The path to the directory where the app executable is located.
*
* @type {string}
*/
get app() {
return this.getLocation('app');
}
/**
* The project root path.
*
* @type {string}
*/
get home() {
return this.getLocation('home');
}
/**
* A dictionary of different path locations.
*
* @type {Object.<string, string>}
* @access protected
* @ignore
*/
get locations() {
return this._locations;
}
/**
* The root path from where the app is being executed.
*
* @type {string}
* @access protected
* @ignore
*/
get path() {
return this._path;
}
/**
* Finds and register the location for the app executable directory.
*
* @access protected
* @ignore
*/
_addAppLocation() {
let current = module;
while (current.parent && current.parent.filename !== current.filename) {
current = current.parent;
}
this.addLocation('app', path.dirname(current.filename));
}
}
/**
* The service provider to register an instance of {@link PathUtils} on the container.
*
* @type {ProviderCreator<PathUtilsProviderOptions>}
* @tutorial pathUtils
*/
const pathUtils = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'pathUtils', () => new PathUtils(options.home));
});
module.exports.PathUtils = PathUtils;
module.exports.pathUtils = pathUtils;