UNPKG

@commenthol/app-config

Version:

set values in global app configuration

148 lines (133 loc) 3.38 kB
import { homedir } from 'node:os' import * as path from 'node:path' import fsp from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { get, set } from 'mergee' import { parse as vParse } from 'valibot' import { v, StringSchema } from '#validate.js' /** @typedef {import('valibot').BaseSchema} ValibotSchema */ /** * @param {string|URL|undefined} urlOrString * @returns {string|undefined} */ const toFilename = (urlOrString) => urlOrString instanceof URL ? fileURLToPath(urlOrString) : urlOrString /** * @param {string} appName application name * @param {string} [filename] config filename with extension * @returns {string} */ export const getGlobalConfigPathname = (appName, filename) => { const _filename = filename || 'config.json' switch (process.platform) { case 'win32': return path.resolve( homedir(), 'AppData', 'Local', 'Packages', appName, _filename ) case 'darwin': return path.resolve( homedir(), 'Library', 'Preferences', appName, _filename ) case 'linux': default: return path.resolve(homedir(), '.config', appName, _filename) } } export class AppConfig { /** * @param {{ * appName: string * schema?: Record<string, ValibotSchema> * filename?: string|URL * defaults?: Record<string, any> * }} options */ constructor(options) { const { appName, schema, filename, defaults = {} } = options if (!appName) { throw new Error('appName is missing') } this.schema = schema const _filename = toFilename(filename) this.filename = _filename && path.isAbsolute(_filename) ? _filename : getGlobalConfigPathname(appName, _filename) this.config = structuredClone(defaults) } /** * @param {string} key * @param {string|null|undefined} value */ set(key, value) { const schema = this.schema?.[key] if (this.schema && !schema) { throw new Error(`Unsupported key "${key}"`) } if (['', null, undefined].includes(value)) { // delete entry set(this.config, key, null) } else { // validate and set new value const output = schema ? vParse(v.pipe(StringSchema, schema), value) : value set(this.config, key, output) } } /** * @param {string|string[]} key * @returns {any} */ get(key) { return get(this.config, key) } /** * @private * creates or overwrites file after failures */ async _create() { const dirname = path.dirname(this.filename) await fsp.mkdir(dirname, { recursive: true }) await this.write() } async read() { try { const data = await fsp.readFile(this.filename, 'utf-8') this.config = JSON.parse(data) if (typeof this.config !== 'object') this.config = {} } catch (/** @type {Error|any} */ err) { if (err.code === 'ENOENT') { return } await this._create() } } /** * @returns {Promise<void>} */ async write() { try { await fsp.writeFile( this.filename, JSON.stringify(this.config, null, 2), 'utf-8' ) } catch (/** @type {Error|any} */ err) { if (err.code === 'ENOENT') { await this._create() } else { throw err } } } }