UNPKG

hfs

Version:
257 lines (255 loc) 11.5 kB
"use strict"; // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.showHelp = exports.configFile = exports.saveConfigAsap = exports.currentVersion = exports.versionToScalar = void 0; exports.defineConfig = defineConfig; exports.configKeyExists = configKeyExists; exports.getConfig = getConfig; exports.getWholeConfig = getWholeConfig; exports.setConfig = setConfig; const const_1 = require("./const"); const watchLoad_1 = require("./watchLoad"); const yaml_1 = __importDefault(require("yaml")); const lodash_1 = __importDefault(require("lodash")); const cross_1 = require("./cross"); const debounceAsync_1 = require("./debounceAsync"); const fs_1 = require("fs"); const path_1 = require("path"); const events_1 = __importDefault(require("./events")); const promises_1 = require("fs/promises"); const immer_1 = __importStar(require("immer")); const argv_1 = require("./argv"); (0, immer_1.setAutoFreeze)(false); // we still want to mess with objects later (eg: account.belongs) // keep definition of config properties const configProps = {}; let started = false; // this will tell the difference for subscribeConfig()s that are called before or after config is loaded let state = {}; // current state of config properties const filePath = (0, cross_1.with_)(argv_1.argv.config || process.env.HFS_CONFIG, p => { if (!p) return const_1.CONFIG_FILE; p = (0, path_1.resolve)(const_1.ORIGINAL_CWD, p); try { if ((0, fs_1.statSync)(p).isDirectory()) // try to detect if path points to a folder, in which case we add the standard filename return (0, path_1.join)(p, const_1.CONFIG_FILE); } catch (_a) { } return p; }); // takes a semver like 1.2.3-alpha1, but alpha and beta numbers must share the number progression exports.versionToScalar = lodash_1.default.memoize((ver) => { // this regexp is supposed to be resistant to optional leading "v" and an optional custom name after a space const res = /^v?(\d+)\.(\d+)\.(\d+)(?:-\D+([0-9.]+))?/.exec(ver); if (!res) return NaN; const [, a, b, c, beta] = res.map(Number); const officialScalar = c + b * 1E3 + a * 1E6; // gives 3 digits for each number const betaScalar = 1 / (1 + beta || Infinity); // beta tends to 0, while non-beta is 0. +1 to make it work even in case of alpha0 return officialScalar - betaScalar; }); class Version extends String { constructor(v) { super(v); this.scalar = (0, exports.versionToScalar)(v); } olderThan(otherVersion) { return this.scalar < (0, exports.versionToScalar)(otherVersion); } } const CONFIG_CHANGE_EVENT_PREFIX = 'config.'; exports.currentVersion = new Version(const_1.VERSION); const configVersion = defineConfig('version', const_1.VERSION, v => new Version(v)); function defineConfig(k, defaultValue, compiler) { configProps[k] = { defaultValue }; let compiled = compiler === null || compiler === void 0 ? void 0 : compiler(defaultValue, { k, version: exports.currentVersion, defaultValue }); const ret = { key() { return k; }, get() { return getConfig(k); }, sub(cb) { if (started) // initial event already passed, we'll make the first call cb(getConfig(k), { k, was: defaultValue, defaultValue, version: configVersion.compiled() }); return events_1.default.on(CONFIG_CHANGE_EVENT_PREFIX + k, (v, was, version) => { if (stack.includes(cb)) return; // avoid infinite loop in case a subscriber changes the value stack.push(cb); try { return cb(v, { k, was, version, defaultValue }); } finally { stack.pop(); } }, { warnAfter: 1000 }); // e.g. each plugin watch enable_plugins }, set(v) { if (typeof v === 'function') this.set((0, immer_1.default)(this.get(), v)); else setConfig1(k, v); }, compiled: () => (compiler ? compiled : (0, cross_1.throw_)("missing compiler")), }; if (compiler) ret.sub((...args) => compiled = compiler(...args)); return ret; } function configKeyExists(k) { return configProps.hasOwnProperty(k); } const stack = []; function getConfig(k) { var _a, _b; return (_a = state[k]) !== null && _a !== void 0 ? _a : lodash_1.default.cloneDeep((_b = configProps[k]) === null || _b === void 0 ? void 0 : _b.defaultValue); // clone to avoid changing } function getWholeConfig({ omit, only }) { const defs = (0, cross_1.newObj)(configProps, x => x.defaultValue); let copy = lodash_1.default.defaults({}, state, defs); if (omit === null || omit === void 0 ? void 0 : omit.length) copy = lodash_1.default.omit(copy, omit); if (only) copy = lodash_1.default.pick(copy, only); return lodash_1.default.cloneDeep(copy); } // pass a value to `save` to force saving decision, or leave undefined for auto. Passing false will also reset previously loaded configs. function setConfig(newCfg, save) { const version = lodash_1.default.isString(newCfg.version) ? new Version(newCfg.version) : undefined; const considerEnvs = !process.env['HFS_ENV_BOOTSTRAP'] || !started && lodash_1.default.isEmpty(newCfg); // first time we consider also CLI args const argCfg = !started && lodash_1.default.pickBy((0, cross_1.newObj)(configProps, (x, k) => { var _a; return (_a = argv_1.argv[k]) !== null && _a !== void 0 ? _a : (0, cross_1.tryJson)(considerEnvs ? process.env['HFS_' + k.toUpperCase().replaceAll('-', '_')] : '', lodash_1.default.identity); }), x => x !== undefined); if (!lodash_1.default.isEmpty(argCfg)) { (0, exports.saveConfigAsap)(); // don't set `save` argument, as it would interfere below at check `save===false` Object.assign(newCfg, argCfg); } for (const k in newCfg) apply(k, newCfg[k]); if (save) { (0, exports.saveConfigAsap)(); return; } if (started) { if (save === false) // false is used when loading whole config, and in such case we should not leave previous values untreated. Also, we need this only after we already `started`. for (const k of Object.keys(state)) if (!newCfg.hasOwnProperty(k)) apply(k, undefined); return; } // first time we emit also for the default values for (const k of Object.keys(configProps)) if (!newCfg.hasOwnProperty(k)) apply(k, newCfg[k], true); started = true; events_1.default.emit('configReady', startedWithoutConfig); if (version !== const_1.VERSION) // be sure to save version (0, exports.saveConfigAsap)(); function apply(k, newV, isDefault = false) { return setConfig1(k, newV, save === undefined, argCfg && k in argCfg || isDefault ? exports.currentVersion : version); } } function setConfig1(k, newV, saveChanges = true, valueVersion) { var _a; if (lodash_1.default.isPlainObject(newV)) newV = lodash_1.default.pickBy(newV, x => x !== undefined); const def = (_a = configProps[k]) === null || _a === void 0 ? void 0 : _a.defaultValue; if (same(newV !== null && newV !== void 0 ? newV : null, def !== null && def !== void 0 ? def : null)) newV = undefined; if (started && same(newV, state[k])) return; // no change const was = getConfig(k); // include cloned default, if necessary state[k] = newV; events_1.default.emit(CONFIG_CHANGE_EVENT_PREFIX + k, getConfig(k), was, valueVersion); if (saveChanges) (0, exports.saveConfigAsap)(); function same(a, b) { return a === b || JSON.stringify(a) === JSON.stringify(b); } } const saveDebounced = (0, debounceAsync_1.debounceAsync)(async () => { while (!started) await (0, cross_1.wait)(100); // keep backup const bak = filePath + '.bak'; const aWeekAgo = Date.now() - cross_1.DAY * 7; if (await (0, promises_1.stat)(bak).then(x => aWeekAgo > x.mtimeMs, () => true)) await (0, promises_1.copyFile)(filePath, bak).catch(() => { }); // ignore errors await exports.configFile.save(stringify({ ...state, version: const_1.VERSION })) .catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err))); }); const saveConfigAsap = () => void saveDebounced(); exports.saveConfigAsap = saveConfigAsap; function stringify(obj) { return yaml_1.default.stringify(obj, { lineWidth: 1000 }); } let startedWithoutConfig = false; console.log("config", filePath); exports.configFile = (0, watchLoad_1.watchLoad)(filePath, text => { startedWithoutConfig = !text; try { setConfig(yaml_1.default.parse(text, { uniqueKeys: false }) || {}, false); } catch (e) { console.error("Error in", filePath, ':', e.message || String(e)); } }, { failedOnFirstAttempt() { startedWithoutConfig = true; console.log("No config file, using defaults"); setTimeout(() => // this is called synchronously, but we need to call setConfig after first tick, when all configs are defined setConfig({}, false)); } }); exports.showHelp = argv_1.argv.help; events_1.default.on('configReady', () => { if (!exports.showHelp) return; console.log(`HELP You can pass any configuration in the form: --name value Most common configurations: --create-admin <password> --port <port> --cert <path> --private_key <path> --consoleFile <path> For a description of each configuration, please refer to https://rejetto.com/hfs-config Other options: --debug will print extra information `); process.exit(0); });