import * as fs from 'fs'; import * as nodePath from 'path'; import { hasRef, resolveRefs } from '../builder/transformFileSettings'; import { getFsStats, merge } from '../lib'; import { ConfigSettingsPath } from '../types'; import { FileConfigNodeFileResourceService } from './FileConfigNodeFileResource'; import { ProfilesConfigNodeBase } from './ProfilesConfigNodeBase'; import { ConfigNodePropertyFactory, ConfigPropertyNodeFactoryArgs, FileConfigNodeArgs, FileConfigPropertyNodeFactoryArgs, ProfileFileConfigNodeOptions, } from './types'; const defaultOptions: ProfileFileConfigNodeOptions = { profilesDirBaseName: 'profiles', settingsBaseName: 'settings', }; /** * Extends ProfileConfig Node to load configuration data from file system path that directly maps * to property paths for a configuration object. Also features a fileResources method to support * reference to arbitrary files residing within a 'resources' directory under the file system path of this Node. */ export class ProfilesFileConfigNodeBase extends ProfilesConfigNodeBase< TValue, ProfileFileConfigNodeOptions, FileConfigPropertyNodeFactoryArgs > { public get path(): string { if (!this._path && this._pathArg) { const stats = getFsStats(this._pathArg); if (stats) { this._path = stats.isDirectory() ? this._pathArg : nodePath.dirname(this._pathArg); } else { return this._pathArg; } } return this._path; } public get fileResources(): FileConfigNodeFileResourceService { if (!this._fileResources) { this._fileResources = new FileConfigNodeFileResourceService(this, this.options.resourceOptions); } return this._fileResources; } private _pathArg: string; private _path: string; private _fileResources: FileConfigNodeFileResourceService; protected constructor( args: | FileConfigPropertyNodeFactoryArgs | FileConfigNodeArgs ) { super(args); this._pathArg = args.path; this.options = merge([this.options, defaultOptions, args.options]); } private get pathExists(): boolean { return this.path && fs.existsSync(this.path); } protected async getInitialValue() { if ( this._initialValue && this.options.initialValueRefRootDir && fs.existsSync(this.options.initialValueRefRootDir) ) { // if (hasRef(this._initialValue)) { // debugger; // } const resolved = await resolveRefs(this._initialValue, this.options.initialValueRefRootDir); return resolved; } else { return this._initialValue; } } protected async afterInit() { if (this.pathExists) { // load settings at path and apply value to this node const fileConfigSettingsPath: ConfigSettingsPath = { baseName: this.options.settingsBaseName, rootPath: this.path, }; await this._applyValue(fileConfigSettingsPath); // load a node for any subdirectories under this path await this.applySubDirPlaceholders(); } } protected get defaultPropertyNodeFactory(): ConfigNodePropertyFactory { return async (args: FileConfigPropertyNodeFactoryArgs): Promise => { const node = new ProfilesFileConfigNodeBase(args); await node.init(); return node; }; } protected getPropertyNodeFactoryArgs(baseArgs: ConfigPropertyNodeFactoryArgs): FileConfigPropertyNodeFactoryArgs { return { ...baseArgs, path: this.path ? nodePath.join(this.path, baseArgs.propertyName) : undefined, }; } protected get excludedConfigDirectoryNames(): string[] { return [this.fileResources.options.dirBaseName, this.options.settingsBaseName]; } /** * Loads an empty config entry/property for each subdirectory under this node's root. The sub-dir/node will * be loaded as each node is parsed and loaded during init */ private async applySubDirPlaceholders() { // If this path exists and it is a profiles directory, load sub-paths if (fs.existsSync(this.path)) { // && nodePath.basename(this.path) === this.options.profilesDirBaseName) { // get profile names from file system under profiles directory const dirNames = fs .readdirSync(this.path) .filter(f => { const fPath = nodePath.join(this.path, f); // assume all directories, and files that aren't the settings baseName return fs.statSync(fPath).isDirectory() && this.excludedConfigDirectoryNames.indexOf(f) < 0; }) .map(f => nodePath.parse(f).name); if (dirNames.length > 0) { // create a placeholder for each profile (will be loaded with the profiles node) const dirNameConfig = dirNames.reduce((acc, curr) => { acc[curr] = {}; return acc; }, {}); // set loaded profiles await this._applyValue(dirNameConfig); } } } }