UNPKG

yeoman-generator

Version:

Rails-inspired generator system that provides scaffolding for your apps

268 lines (267 loc) 8.01 kB
import assert from 'node:assert'; import { cloneDeep, defaults as setDefaults, merge, get, set } from 'lodash-es'; import sortKeys from 'sort-keys'; /** * Proxy handler for Storage */ const proxyHandler = { get(storage, property, receiver) { return storage.get(property); }, set(storage, property, value, receiver) { if (typeof property === 'string') { storage.set(property, value); return true; } return false; }, ownKeys(storage) { return Reflect.ownKeys(storage._store); }, has(storage, prop) { return storage.get(prop) !== undefined; }, getOwnPropertyDescriptor(storage, key) { return { get: () => this.get(storage, key, null), enumerable: true, configurable: true, }; }, }; /** * Storage instances handle a json file where Generator authors can store data. * * The `Generator` class instantiate the storage named `config` by default. * * @constructor * @param name The name of the new storage (this is a namespace) * @param fs A mem-fs editor instance * @param configPath The filepath used as a storage. * * @example * class extend Generator { * writing: function() { * this.config.set('coffeescript', false); * } * } */ class Storage { path; name; fs; indent; lodashPath; disableCache; disableCacheByFile; sorted; existed; _cachedStore; constructor(name, fs, configPath, options = {}) { let editor; let actualName; let actualConfigPath; let actualOptions = options; if (typeof name === 'string') { actualName = name; } else if (typeof name === 'object') { editor = name; } if (typeof fs === 'string') { actualConfigPath = fs; } else { editor = fs; } if (typeof configPath === 'string') { actualConfigPath = configPath; } else if (typeof configPath === 'object') { actualOptions = configPath; } if (!editor) { throw new Error(`Check parameters`); } assert(actualConfigPath, 'A config filepath is required to create a storage'); this.path = actualConfigPath; this.name = actualName ?? actualOptions.name; this.fs = editor; this.indent = 2; this.lodashPath = actualOptions.lodashPath ?? false; this.disableCache = actualOptions.disableCache ?? false; this.disableCacheByFile = actualOptions.disableCacheByFile ?? false; this.sorted = actualOptions.sorted ?? false; this.existed = Object.keys(this._store).length > 0; this.fs.store.on('change', filename => { // At mem-fs 1.1.3 filename is not passed to the event. if (this.disableCacheByFile || (filename && filename !== this.path)) { return; } delete this._cachedStore; }); } /** * @protected * @return the store content */ readContent() { const content = this.fs.readJSON(this.path, {}); if (!content || typeof content !== 'object' || Array.isArray(content)) { throw new Error(`${this.path} is not a valid Storage`); } return content; } /** * @protected * @return the store content */ writeContent(fullStore) { return this.fs.writeJSON(this.path, fullStore, undefined, this.indent); } /** * Return the current store as JSON object * @return the store content * @private */ get _store() { const store = this._cachedStore ?? this.readContent(); if (!this.disableCache) { this._cachedStore = store; } if (!this.name) { return store; } return ((this.lodashPath ? get(store, this.name) : store[this.name]) ?? {}); } /** * Persist a configuration to disk * @param val - current configuration values * @private */ _persist(value) { if (this.sorted) { value = sortKeys(value, { deep: true }); } let fullStore; if (this.name) { fullStore = this.readContent(); if (this.lodashPath) { set(fullStore, this.name, value); } else { fullStore[this.name] = value; } } else { fullStore = value; } this.writeContent(fullStore); } /** * Save a new object of values */ save() { this._persist(this._store); } /** * Get a stored value * @param key The key under which the value is stored. * @return The stored value. Any JSON valid type could be returned */ get(key) { return this._store[key]; } /** * Get a stored value from a lodash path * @param path The path under which the value is stored. * @return The stored value. Any JSON valid type could be returned */ getPath(path) { return get(this._store, path); } /** * Get all the stored values * @return key-value object */ getAll() { return cloneDeep(this._store); } set(key, value) { const store = this._store; if (typeof key === 'object') { value = Object.assign(store, key); } else if (typeof key === 'string' || typeof key === 'number') { store[key] = value; } else { throw new TypeError(`key not supported ${typeof key}`); } this._persist(store); return value; } /** * Assign a lodash path to a value and schedule a save. * @param path The key under which the value is stored * @param val Any valid JSON type value (String, Number, Array, Object). * @return val Whatever was passed in as val. */ setPath(path, value) { assert(typeof value !== 'function', "Storage value can't be a function"); const store = this._store; set(store, path, value); this._persist(store); return value; } /** * Delete a key from the store and schedule a save. * @param key The key under which the value is stored. */ delete(key) { const store = this._store; // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete store[key]; this._persist(store); } /** * Setup the store with defaults value and schedule a save. * If keys already exist, the initial value is kept. * @param defaults Key-value object to store. * @return val Returns the merged options. */ defaults(defaults) { assert(typeof defaults === 'object', 'Storage `defaults` method only accept objects'); const store = setDefaults({}, this._store, defaults); this._persist(store); return this.getAll(); } /** * @param defaults Key-value object to store. * @return val Returns the merged object. */ merge(source) { assert(typeof source === 'object', 'Storage `merge` method only accept objects'); const value = merge({}, this._store, source); this._persist(value); return this.getAll(); } /** * Create a child storage. * @param path - relative path of the key to create a new storage. * Some paths need to be escaped. Eg: ["dotted.path"] * @return Returns a new Storage. */ createStorage(path) { const childName = this.name ? `${this.name}.${path}` : path; return new Storage(childName, this.fs, this.path, { lodashPath: true }); } /** * Creates a proxy object. * @return proxy. */ createProxy() { return new Proxy(this, proxyHandler); } } export default Storage;