Compare commits

...

3 Commits

Author SHA1 Message Date
Derrick Hammer 257e053f9f
*Update dist 2022-12-19 11:30:08 -05:00
Derrick Hammer f70348f055
*Update dep 2022-12-19 11:29:59 -05:00
Derrick Hammer afb5389243
*Heavily refactor and simplification
*Support dot notation in accessing and creating settings
*Ensure we only load .json files
2022-12-19 11:29:03 -05:00
4 changed files with 125 additions and 1031 deletions

49
dist/index.d.ts vendored
View File

@ -4,46 +4,22 @@
* https://github.com/bcoin-org/bcoin * https://github.com/bcoin-org/bcoin
*/ */
/// <reference types="node" /> /// <reference types="node" />
export interface Options { import arg from "arg";
suffix?: string;
fallback?: string;
alias?: object;
}
export interface LoadOptions {
hash?: string | boolean;
query?: string | boolean;
env?: object | boolean;
argv?: string[] | boolean;
config?: string | boolean;
}
/** /**
* Config Parser * Config Parser
*/ */
export default class Config { export default class Config {
private module; private module;
private prefix;
private suffix?;
private fallback?;
private options;
private alias;
private data; private data;
private env; constructor(module: string);
private args;
private argv;
private pass;
private query;
private hash;
constructor(module: string, options?: Options);
private init;
inject(options: object): void; inject(options: object): void;
load(options: LoadOptions): void; load(): void;
open(file: string): void;
openDir(dir: string): void; openDir(dir: string): void;
openJson(file: string): void; open(file: string): void;
saveConfigJson(file: string, data: object): void; save(file: string, data: object): void;
filter(name: string): Config;
set(key: string, value: any): void; set(key: string, value: any): void;
has(key: string): boolean; has(key: string): any;
private normalize;
get(key: string, fallback?: any): any; get(key: string, fallback?: any): any;
typeOf(key: string): "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | "null"; typeOf(key: string): "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | "null";
str(key: string, fallback?: any): any; str(key: string, fallback?: any): any;
@ -58,16 +34,7 @@ export default class Config {
array(key: string, fallback?: any): any; array(key: string, fallback?: any): any;
obj(key: string, fallback?: any): any; obj(key: string, fallback?: any): any;
func(key: string, fallback?: any): any; func(key: string, fallback?: any): any;
path(key: string, fallback?: any): any;
mb(key: string, fallback?: any): any; mb(key: string, fallback?: any): any;
getSuffix(): any; parseArg(args: arg.Result<any>): void;
getPrefix(): string;
getFile(file: string): any;
location(file: string): string;
parseConfig(text: string): void;
parseArg(argv?: string[]): void;
parseEnv(env?: object): void; parseEnv(env?: object): void;
parseQuery(query: string): void | {};
parseHash(hash: string): void | {};
parseForm(query: string, ch: string, map: object): void;
} }

483
dist/index.js vendored
View File

@ -10,60 +10,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const bsert_1 = __importDefault(require("bsert")); const bsert_1 = __importDefault(require("bsert"));
const path_1 = __importDefault(require("path")); const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const fs_1 = __importDefault(require("fs")); const fs_1 = __importDefault(require("fs"));
const HOME = os_1.default.homedir ? os_1.default.homedir() : "/"; const arg_1 = __importDefault(require("arg"));
const object_path_1 = __importDefault(require("object-path"));
const deep_to_flat_object_1 = __importDefault(require("deep-to-flat-object"));
/** /**
* Config Parser * Config Parser
*/ */
class Config { class Config {
module; module;
prefix;
suffix;
fallback;
options = {};
alias = {};
data = {}; data = {};
env = {}; constructor(module) {
args = {};
argv = [];
pass = [];
query = {};
hash = {};
constructor(module, options) {
(0, bsert_1.default)(typeof module === "string"); (0, bsert_1.default)(typeof module === "string");
(0, bsert_1.default)(module.length > 0); (0, bsert_1.default)(module.length > 0);
this.module = module; this.module = module;
this.prefix = path_1.default.join(HOME, `.${module}`);
if (options) {
this.init(options);
}
}
init(options) {
(0, bsert_1.default)(options && typeof options === "object");
if (options.suffix != null) {
(0, bsert_1.default)(typeof options.suffix === "string");
this.suffix = options.suffix;
}
if (options.fallback != null) {
(0, bsert_1.default)(typeof options.fallback === "string");
this.fallback = options.fallback;
}
if (options.alias) {
(0, bsert_1.default)(typeof options.alias === "object");
for (const key of Object.keys(options.alias)) {
const value = options.alias[key];
(0, bsert_1.default)(typeof value === "string");
this.alias[key] = value;
}
}
} }
inject(options) { inject(options) {
for (const key of Object.keys(options)) { for (const key of Object.keys(options)) {
const value = options[key]; const value = options[key];
switch (key) { switch (key) {
case "hash":
case "query":
case "env": case "env":
case "argv": case "argv":
case "config": case "config":
@ -72,41 +37,19 @@ class Config {
this.set(key, value); this.set(key, value);
} }
} }
load(options) { load() {
if (options.hash) { const args = (0, arg_1.default)({}, { permissive: true });
this.parseHash(options.hash); this.parseArg(args);
}
if (options.query) {
this.parseQuery(options.query);
}
if (options.env) {
this.parseEnv(options.env);
}
if (options.argv) {
this.parseArg(options.argv);
}
this.prefix = this.getPrefix();
}
open(file) {
const path = this.getFile(file);
let text;
try {
text = fs_1.default.readFileSync(path, "utf8");
}
catch (e) {
if (e.code === "ENOENT")
return;
throw e;
}
this.parseConfig(text);
this.prefix = this.getPrefix();
} }
openDir(dir) { openDir(dir) {
(0, bsert_1.default)(fs_1.default.existsSync(dir), `Directory ${dir} does not exist`); (0, bsert_1.default)(fs_1.default.existsSync(dir), `Directory ${dir} does not exist`);
let files = fs_1.default.readdirSync(dir).map((item) => path_1.default.join(dir, item)); let files = fs_1.default
files.forEach(this.openJson.bind(this)); .readdirSync(dir)
.filter((item) => item.endsWith(".json"))
.map((item) => path_1.default.join(dir, item));
files.forEach(this.open.bind(this));
} }
openJson(file) { open(file) {
let json; let json;
try { try {
json = fs_1.default.readFileSync(file, "utf8"); json = fs_1.default.readFileSync(file, "utf8");
@ -118,20 +61,22 @@ class Config {
throw new Error(`Error parsing file ${file}: ${e.message}`); throw new Error(`Error parsing file ${file}: ${e.message}`);
} }
(0, bsert_1.default)(typeof json === "object", `Config file ${file} must be an object`); (0, bsert_1.default)(typeof json === "object", `Config file ${file} must be an object`);
for (const key of Object.keys(json)) { const settings = (0, deep_to_flat_object_1.default)(json);
for (const key of Object.keys(settings)) {
const value = json[key]; const value = json[key];
switch (true) { let keyPath = key.split(".");
case Array.isArray(value): let isArray = typeof parseInt(keyPath.pop()) === "number";
this.set(key, [...(this.array(key) ?? []), ...value]); if (isArray) {
break; let itemPath = keyPath.join(".");
default: let item = this.get(itemPath, []);
this.set(key, value); item.push(value);
break; this.set(itemPath, item);
continue;
} }
this.set(key, value);
} }
this.prefix = this.getPrefix();
} }
saveConfigJson(file, data) { save(file, data) {
(0, bsert_1.default)(typeof data === "object"); (0, bsert_1.default)(typeof data === "object");
(0, bsert_1.default)(!Array.isArray(data)); (0, bsert_1.default)(!Array.isArray(data));
const configDir = this.str("configdir"); const configDir = this.str("configdir");
@ -140,56 +85,34 @@ class Config {
fs_1.default.mkdirSync(configDir, { recursive: true }); fs_1.default.mkdirSync(configDir, { recursive: true });
} }
fs_1.default.writeFileSync(fullPath, JSON.stringify(data)); fs_1.default.writeFileSync(fullPath, JSON.stringify(data));
this.openJson(fullPath); this.open(fullPath);
}
filter(name) {
(0, bsert_1.default)(typeof name === "string");
const child = new Config(this.module);
child.prefix = this.prefix;
child.suffix = this.suffix;
child.fallback = this.fallback;
child.argv = this.argv;
child.pass = this.pass;
_filter(name, this.env, child.env);
_filter(name, this.args, child.args);
_filter(name, this.query, child.query);
_filter(name, this.hash, child.hash);
_filter(name, this.options, child.options);
return child;
} }
set(key, value) { set(key, value) {
(0, bsert_1.default)(typeof key === "string", "Key must be a string."); (0, bsert_1.default)(typeof key === "string", "Key must be a string.");
if (value == null) { if (value == null) {
return; return;
} }
key = key.replace(/-/g, ""); key = this.normalize(key);
key = key.toLowerCase(); object_path_1.default.set(this.data, key, value);
this.options[key] = value; this.data[key] = value;
} }
has(key) { has(key) {
if (typeof key === "number") {
(0, bsert_1.default)(key >= 0, "Index must be positive.");
return key < this.argv.length;
}
(0, bsert_1.default)(typeof key === "string", "Key must be a string."); (0, bsert_1.default)(typeof key === "string", "Key must be a string.");
key = key.replace(/-/g, ""); key = key.replace(/-/g, "");
key = key.toLowerCase(); key = key.toLowerCase();
if (key in this.hash && this.hash[key] !== null) { return object_path_1.default.has(this.data, key);
return true; }
normalize(key, env = false) {
(0, bsert_1.default)(typeof key === "string", "Key must be a string.");
if (env) {
key = key.replace(/__/g, ".");
key = key.replace(/_/g, "");
} }
if (key in this.query && this.query[key] !== null) { else {
return true; key = key.replace(/-/g, "");
} }
if (key in this.args && this.args[key] !== null) { key = key.toLowerCase();
return true; return key;
}
if (key in this.env && this.env[key] !== null) {
return true;
}
if (key in this.data && this.data[key] !== null) {
return true;
}
return this.options[key] !== null;
} }
get(key, fallback = null) { get(key, fallback = null) {
if (Array.isArray(key)) { if (Array.isArray(key)) {
@ -202,38 +125,9 @@ class Config {
} }
return fallback; return fallback;
} }
if (typeof key === "number") {
(0, bsert_1.default)(key >= 0, "Index must be positive.");
if (key >= this.argv.length) {
return fallback;
}
if (this.argv[key] != null) {
return this.argv[key];
}
return fallback;
}
(0, bsert_1.default)(typeof key === "string", "Key must be a string."); (0, bsert_1.default)(typeof key === "string", "Key must be a string.");
key = key.replace(/-/g, ""); key = this.normalize(key);
key = key.toLowerCase(); return object_path_1.default.get(this.data, key);
if (key in this.hash && this.hash[key] !== null) {
return this.hash[key];
}
if (key in this.query && this.query[key] !== null) {
return this.query[key];
}
if (key in this.args && this.args[key] !== null) {
return this.args[key];
}
if (key in this.env && this.env[key] !== null) {
return this.env[key];
}
if (key in this.data && this.data[key] !== null) {
return this.data[key];
}
if (key in this.options && this.options[key] !== null) {
return this.options[key];
}
return fallback;
} }
typeOf(key) { typeOf(key) {
const value = this.get(key); const value = this.get(key);
@ -429,26 +323,6 @@ class Config {
} }
return value; return value;
} }
path(key, fallback = null) {
let value = this.str(key);
if (value === null) {
return fallback;
}
if (value.length === 0) {
return fallback;
}
switch (value[0]) {
case "~": // home dir
value = path_1.default.join(HOME, value.substring(1));
break;
case "@": // prefix
value = path_1.default.join(this.prefix, value.substring(1));
break;
default: // cwd
break;
}
return path_1.default.normalize(value);
}
mb(key, fallback = null) { mb(key, fallback = null) {
const value = this.uint(key); const value = this.uint(key);
if (value === null) { if (value === null) {
@ -456,176 +330,10 @@ class Config {
} }
return value * 1024 * 1024; return value * 1024 * 1024;
} }
getSuffix() { parseArg(args) {
if (!this.suffix) { for (let key in args) {
throw new Error("No suffix presented."); let newKey = key.replace("-", "");
} object_path_1.default.set(this.data, newKey, args[key]);
const suffix = this.str(this.suffix, this.fallback);
(0, bsert_1.default)(isAlpha(suffix), "Bad suffix.");
return suffix;
}
getPrefix() {
let prefix = this.str("prefix");
if (prefix) {
if (prefix[0] === "~") {
prefix = path_1.default.join(HOME, prefix.substring(1));
}
}
else {
prefix = path_1.default.join(HOME, `.${this.module}`);
}
if (this.suffix) {
const suffix = this.str(this.suffix);
if (suffix) {
(0, bsert_1.default)(isAlpha(suffix), "Bad suffix.");
if (this.fallback && suffix !== this.fallback) {
prefix = path_1.default.join(prefix, suffix);
}
}
}
return path_1.default.normalize(prefix);
}
getFile(file) {
const name = this.str("config");
if (name) {
return name;
}
return path_1.default.join(this.prefix, file);
}
location(file) {
return path_1.default.join(this.prefix, file);
}
parseConfig(text) {
(0, bsert_1.default)(typeof text === "string", "Config must be text.");
if (text.charCodeAt(0) === 0xfeff) {
text = text.substring(1);
}
text = text.replace(/\r\n/g, "\n");
text = text.replace(/\r/g, "\n");
text = text.replace(/\\\n/g, "");
let num = 0;
for (const chunk of text.split("\n")) {
const line = chunk.trim();
num += 1;
if (line.length === 0) {
continue;
}
if (line[0] === "#") {
continue;
}
const index = line.indexOf(":");
if (index === -1) {
throw new Error(`Expected ':' on line ${num}: "${line}".`);
}
let key = line.substring(0, index).trim();
key = key.replace(/\-/g, "");
if (!isLowerKey(key)) {
throw new Error(`Invalid option on line ${num}: ${key}.`);
}
const value = line.substring(index + 1).trim();
if (value.length === 0) {
continue;
}
const alias = this.alias[key];
if (alias) {
key = alias;
}
this.data[key] = value;
}
}
parseArg(argv) {
if (!argv || typeof argv !== "object")
argv = process.argv;
(0, bsert_1.default)(Array.isArray(argv));
let last = null;
let pass = false;
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
(0, bsert_1.default)(typeof arg === "string");
if (arg === "--") {
pass = true;
continue;
}
if (pass) {
this.pass.push(arg);
continue;
}
if (arg.length === 0) {
last = null;
continue;
}
if (arg.indexOf("--") === 0) {
const index = arg.indexOf("=");
let key = null;
let value = null;
let empty = false;
if (index !== -1) {
// e.g. --opt=val
key = arg.substring(2, index);
value = arg.substring(index + 1);
last = null;
empty = false;
}
else {
// e.g. --opt
key = arg.substring(2);
value = "true";
last = null;
empty = true;
}
key = key.replace(/\-/g, "");
if (!isLowerKey(key)) {
throw new Error(`Invalid argument: --${key}.`);
}
if (value.length === 0) {
continue;
}
// Do not allow one-letter aliases.
if (key.length > 1) {
const alias = this.alias[key];
if (alias) {
key = alias;
}
}
this.args[key] = value;
if (empty) {
last = key;
}
continue;
}
if (arg[0] === "-") {
// e.g. -abc
last = null;
for (let j = 1; j < arg.length; j++) {
let key = arg[j];
if ((key < "a" || key > "z") &&
(key < "A" || key > "Z") &&
(key < "0" || key > "9") &&
key !== "?") {
throw new Error(`Invalid argument: -${key}.`);
}
const alias = this.alias[key];
if (alias) {
key = alias;
}
this.args[key] = "true";
last = key;
}
continue;
}
// e.g. foo
const value = arg;
if (value.length === 0) {
last = null;
continue;
}
if (last) {
this.args[last] = value;
last = null;
}
else {
this.argv.push(value);
}
} }
} }
parseEnv(env) { parseEnv(env) {
@ -640,84 +348,18 @@ class Config {
for (let key of Object.keys(env)) { for (let key of Object.keys(env)) {
const value = env[key]; const value = env[key];
(0, bsert_1.default)(typeof value === "string"); (0, bsert_1.default)(typeof value === "string");
if (key.indexOf(prefix) !== 0) if (key.indexOf(prefix) !== 0) {
continue; continue;
key = key.substring(prefix.length); }
key = key.replace(/_/g, "");
if (!isUpperKey(key)) { if (!isUpperKey(key)) {
continue; continue;
} }
key = key.substring(prefix.length);
key = this.normalize(key, true);
if (value.length === 0) { if (value.length === 0) {
continue; continue;
} }
key = key.toLowerCase(); object_path_1.default.set(this.data, key);
// Do not allow one-letter aliases.
if (key.length > 1) {
const alias = this.alias[key];
if (alias) {
key = alias;
}
}
this.env[key] = value;
}
}
parseQuery(query) {
if (typeof query !== "string") {
if (!global.location) {
return {};
}
query = global.location.search;
if (typeof query !== "string") {
return {};
}
}
return this.parseForm(query, "?", this.query);
}
parseHash(hash) {
if (typeof hash !== "string") {
if (!global.location) {
return {};
}
hash = global.location.hash;
if (typeof hash !== "string") {
return {};
}
}
return this.parseForm(hash, "#", this.hash);
}
parseForm(query, ch, map) {
(0, bsert_1.default)(typeof query === "string");
if (query.length === 0) {
return;
}
if (query[0] === ch) {
query = query.substring(1);
}
for (const pair of query.split("&")) {
const index = pair.indexOf("=");
let key, value;
if (index !== -1) {
key = pair.substring(0, index);
value = pair.substring(index + 1);
}
else {
key = pair;
value = "true";
}
key = unescape(key);
key = key.replace(/\-/g, "");
if (!isLowerKey(key)) {
continue;
}
value = unescape(value);
if (value.length === 0) {
continue;
}
const alias = this.alias[key];
if (alias) {
key = alias;
}
map[key] = value;
} }
} }
} }
@ -734,41 +376,18 @@ function fmt(key) {
} }
return key; return key;
} }
function unescape(str) {
try {
str = decodeURIComponent(str);
str = str.replace(/\+/g, " ");
}
catch (e) { }
str = str.replace(/\0/g, "");
return str;
}
function isAlpha(str) { function isAlpha(str) {
return /^[a-z0-9_\-]+$/i.test(str); return /^[a-z0-9_\-]+$/i.test(str);
} }
function isKey(key) { function isKey(key) {
return /^[a-zA-Z0-9]+$/.test(key); return /^[a-zA-Z0-9]+$/.test(key);
} }
function isLowerKey(key) {
if (!isKey(key)) {
return false;
}
return !/[A-Z]/.test(key);
}
function isUpperKey(key) { function isUpperKey(key) {
if (!isKey(key)) { if (!isKey(key)) {
return false; return false;
} }
return !/[a-z]/.test(key); return !/[a-z]/.test(key);
} }
function _filter(name, a, b) {
for (const key of Object.keys(a)) {
if (key.length > name.length && key.indexOf(name) === 0) {
const sub = key.substring(name.length);
b[sub] = a[key];
}
}
}
function fromFloat(num, exp) { function fromFloat(num, exp) {
(0, bsert_1.default)(typeof num === "number" && isFinite(num)); (0, bsert_1.default)(typeof num === "number" && isFinite(num));
(0, bsert_1.default)(Number.isSafeInteger(exp)); (0, bsert_1.default)(Number.isSafeInteger(exp));

View File

@ -12,9 +12,13 @@
"test": "bmocha --reporter spec test/*-test.js" "test": "bmocha --reporter spec test/*-test.js"
}, },
"dependencies": { "dependencies": {
"bsert": "~0.0.10" "arg": "^5.0.2",
"bsert": "~0.0.10",
"deep-to-flat-object": "^1.0.1",
"object-path": "^0.11.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.7.18" "@types/node": "^18.7.18",
"typescript": "^4.9.4"
} }
} }

View File

@ -8,24 +8,10 @@
import assert from "bsert"; import assert from "bsert";
import Path from "path"; import Path from "path";
import os from "os";
import fs from "fs"; import fs from "fs";
import arg from "arg";
const HOME = os.homedir ? os.homedir() : "/"; import objectPath from "object-path";
import deepToFlatObject from "deep-to-flat-object";
export interface Options {
suffix?: string;
fallback?: string;
alias?: object;
}
export interface LoadOptions {
hash?: string | boolean;
query?: string | boolean;
env?: object | boolean;
argv?: string[] | boolean;
config?: string | boolean;
}
/** /**
* Config Parser * Config Parser
@ -33,52 +19,13 @@ export interface LoadOptions {
export default class Config { export default class Config {
private module: string; private module: string;
private prefix: string;
private suffix?: string;
private fallback?: string;
private options: Options = {};
private alias = {};
private data = {}; private data = {};
private env = {};
private args = {};
private argv: any[] = [];
private pass: any[] = [];
private query = {};
private hash = {};
constructor(module: string, options?: Options) { constructor(module: string) {
assert(typeof module === "string"); assert(typeof module === "string");
assert(module.length > 0); assert(module.length > 0);
this.module = module; this.module = module;
this.prefix = Path.join(HOME, `.${module}`);
if (options) {
this.init(options);
}
}
private init(options: Options) {
assert(options && typeof options === "object");
if (options.suffix != null) {
assert(typeof options.suffix === "string");
this.suffix = options.suffix;
}
if (options.fallback != null) {
assert(typeof options.fallback === "string");
this.fallback = options.fallback;
}
if (options.alias) {
assert(typeof options.alias === "object");
for (const key of Object.keys(options.alias)) {
const value = options.alias[key];
assert(typeof value === "string");
this.alias[key] = value;
}
}
} }
public inject(options: object) { public inject(options: object) {
@ -86,8 +33,6 @@ export default class Config {
const value = options[key]; const value = options[key];
switch (key) { switch (key) {
case "hash":
case "query":
case "env": case "env":
case "argv": case "argv":
case "config": case "config":
@ -98,50 +43,23 @@ export default class Config {
} }
} }
public load(options: LoadOptions) { public load() {
if (options.hash) { const args = arg({}, { permissive: true });
this.parseHash(options.hash as string);
}
if (options.query) { this.parseArg(args);
this.parseQuery(options.query as string);
}
if (options.env) {
this.parseEnv(options.env as object);
}
if (options.argv) {
this.parseArg(options.argv as string[]);
}
this.prefix = this.getPrefix();
}
public open(file: string) {
const path = this.getFile(file);
let text;
try {
text = fs.readFileSync(path, "utf8");
} catch (e) {
if (e.code === "ENOENT") return;
throw e;
}
this.parseConfig(text);
this.prefix = this.getPrefix();
} }
public openDir(dir: string) { public openDir(dir: string) {
assert(fs.existsSync(dir), `Directory ${dir} does not exist`); assert(fs.existsSync(dir), `Directory ${dir} does not exist`);
let files = fs.readdirSync(dir).map((item) => Path.join(dir, item)); let files = fs
files.forEach(this.openJson.bind(this)); .readdirSync(dir)
.filter((item) => item.endsWith(".json"))
.map((item) => Path.join(dir, item));
files.forEach(this.open.bind(this));
} }
public openJson(file: string) { public open(file: string) {
let json; let json;
try { try {
json = fs.readFileSync(file, "utf8"); json = fs.readFileSync(file, "utf8");
@ -153,23 +71,27 @@ export default class Config {
assert(typeof json === "object", `Config file ${file} must be an object`); assert(typeof json === "object", `Config file ${file} must be an object`);
for (const key of Object.keys(json)) { const settings = deepToFlatObject(json);
for (const key of Object.keys(settings)) {
const value = json[key]; const value = json[key];
switch (true) { let keyPath = key.split(".");
case Array.isArray(value): let isArray = typeof parseInt(keyPath.pop()) === "number";
this.set(key, [...(this.array(key) ?? []), ...value]);
break;
default:
this.set(key, value);
break;
}
}
this.prefix = this.getPrefix(); if (isArray) {
let itemPath = keyPath.join(".");
let item = this.get(itemPath, []);
item.push(value);
this.set(itemPath, item);
continue;
}
this.set(key, value);
}
} }
public saveConfigJson(file: string, data: object) { public save(file: string, data: object) {
assert(typeof data === "object"); assert(typeof data === "object");
assert(!Array.isArray(data)); assert(!Array.isArray(data));
@ -181,27 +103,7 @@ export default class Config {
} }
fs.writeFileSync(fullPath, JSON.stringify(data)); fs.writeFileSync(fullPath, JSON.stringify(data));
this.openJson(fullPath); this.open(fullPath);
}
public filter(name: string) {
assert(typeof name === "string");
const child = new Config(this.module);
child.prefix = this.prefix;
child.suffix = this.suffix;
child.fallback = this.fallback;
child.argv = this.argv;
child.pass = this.pass;
_filter(name, this.env, child.env);
_filter(name, this.args, child.args);
_filter(name, this.query, child.query);
_filter(name, this.hash, child.hash);
_filter(name, this.options, child.options);
return child;
} }
public set(key: string, value: any) { public set(key: string, value: any) {
@ -211,40 +113,35 @@ export default class Config {
return; return;
} }
key = key.replace(/-/g, ""); key = this.normalize(key);
key = key.toLowerCase();
this.options[key] = value; objectPath.set(this.data, key, value);
this.data[key] = value;
} }
public has(key: string) { public has(key: string) {
if (typeof key === "number") {
assert(key >= 0, "Index must be positive.");
return key < this.argv.length;
}
assert(typeof key === "string", "Key must be a string."); assert(typeof key === "string", "Key must be a string.");
key = key.replace(/-/g, ""); key = key.replace(/-/g, "");
key = key.toLowerCase(); key = key.toLowerCase();
if (key in this.hash && this.hash[key] !== null) { return objectPath.has(this.data, key);
return true; }
}
if (key in this.query && this.query[key] !== null) { private normalize(key: string, env = false): string {
return true; assert(typeof key === "string", "Key must be a string.");
}
if (key in this.args && this.args[key] !== null) { if (env) {
return true; key = key.replace(/__/g, ".");
} key = key.replace(/_/g, "");
if (key in this.env && this.env[key] !== null) { } else {
return true; key = key.replace(/-/g, "");
}
if (key in this.data && this.data[key] !== null) {
return true;
} }
return this.options[key] !== null; key = key.toLowerCase();
return key;
} }
public get(key: string, fallback = null) { public get(key: string, fallback = null) {
@ -259,45 +156,11 @@ export default class Config {
return fallback; return fallback;
} }
if (typeof key === "number") {
assert(key >= 0, "Index must be positive.");
if (key >= this.argv.length) {
return fallback;
}
if (this.argv[key] != null) {
return this.argv[key];
}
return fallback;
}
assert(typeof key === "string", "Key must be a string."); assert(typeof key === "string", "Key must be a string.");
key = key.replace(/-/g, ""); key = this.normalize(key);
key = key.toLowerCase();
if (key in this.hash && this.hash[key] !== null) { return objectPath.get(this.data, key);
return this.hash[key];
}
if (key in this.query && this.query[key] !== null) {
return this.query[key];
}
if (key in this.args && this.args[key] !== null) {
return this.args[key];
}
if (key in this.env && this.env[key] !== null) {
return this.env[key];
}
if (key in this.data && this.data[key] !== null) {
return this.data[key];
}
if (key in this.options && this.options[key] !== null) {
return this.options[key];
}
return fallback;
} }
public typeOf(key: string) { public typeOf(key: string) {
@ -561,30 +424,6 @@ export default class Config {
return value; return value;
} }
public path(key: string, fallback = null) {
let value = this.str(key);
if (value === null) {
return fallback;
}
if (value.length === 0) {
return fallback;
}
switch (value[0]) {
case "~": // home dir
value = Path.join(HOME, value.substring(1));
break;
case "@": // prefix
value = Path.join(this.prefix, value.substring(1));
break;
default: // cwd
break;
}
return Path.normalize(value);
}
public mb(key: string, fallback = null) { public mb(key: string, fallback = null) {
const value = this.uint(key); const value = this.uint(key);
@ -595,233 +434,10 @@ export default class Config {
return value * 1024 * 1024; return value * 1024 * 1024;
} }
public getSuffix() { public parseArg(args: arg.Result<any>) {
if (!this.suffix) { for (let key in args) {
throw new Error("No suffix presented."); let newKey = key.replace("-", "");
} objectPath.set(this.data, newKey, args[key]);
const suffix = this.str(this.suffix, this.fallback);
assert(isAlpha(suffix), "Bad suffix.");
return suffix;
}
public getPrefix() {
let prefix = this.str("prefix");
if (prefix) {
if (prefix[0] === "~") {
prefix = Path.join(HOME, prefix.substring(1));
}
} else {
prefix = Path.join(HOME, `.${this.module}`);
}
if (this.suffix) {
const suffix = this.str(this.suffix);
if (suffix) {
assert(isAlpha(suffix), "Bad suffix.");
if (this.fallback && suffix !== this.fallback) {
prefix = Path.join(prefix, suffix);
}
}
}
return Path.normalize(prefix);
}
public getFile(file: string) {
const name = this.str("config");
if (name) {
return name;
}
return Path.join(this.prefix, file);
}
public location(file: string) {
return Path.join(this.prefix, file);
}
public parseConfig(text: string) {
assert(typeof text === "string", "Config must be text.");
if (text.charCodeAt(0) === 0xfeff) {
text = text.substring(1);
}
text = text.replace(/\r\n/g, "\n");
text = text.replace(/\r/g, "\n");
text = text.replace(/\\\n/g, "");
let num = 0;
for (const chunk of text.split("\n")) {
const line = chunk.trim();
num += 1;
if (line.length === 0) {
continue;
}
if (line[0] === "#") {
continue;
}
const index = line.indexOf(":");
if (index === -1) {
throw new Error(`Expected ':' on line ${num}: "${line}".`);
}
let key = line.substring(0, index).trim();
key = key.replace(/\-/g, "");
if (!isLowerKey(key)) {
throw new Error(`Invalid option on line ${num}: ${key}.`);
}
const value = line.substring(index + 1).trim();
if (value.length === 0) {
continue;
}
const alias = this.alias[key];
if (alias) {
key = alias;
}
this.data[key] = value;
}
}
public parseArg(argv?: string[]) {
if (!argv || typeof argv !== "object") argv = process.argv;
assert(Array.isArray(argv));
let last = null;
let pass = false;
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
assert(typeof arg === "string");
if (arg === "--") {
pass = true;
continue;
}
if (pass) {
this.pass.push(arg);
continue;
}
if (arg.length === 0) {
last = null;
continue;
}
if (arg.indexOf("--") === 0) {
const index = arg.indexOf("=");
let key = null;
let value = null;
let empty = false;
if (index !== -1) {
// e.g. --opt=val
key = arg.substring(2, index);
value = arg.substring(index + 1);
last = null;
empty = false;
} else {
// e.g. --opt
key = arg.substring(2);
value = "true";
last = null;
empty = true;
}
key = key.replace(/\-/g, "");
if (!isLowerKey(key)) {
throw new Error(`Invalid argument: --${key}.`);
}
if (value.length === 0) {
continue;
}
// Do not allow one-letter aliases.
if (key.length > 1) {
const alias = this.alias[key];
if (alias) {
key = alias;
}
}
this.args[key] = value;
if (empty) {
last = key;
}
continue;
}
if (arg[0] === "-") {
// e.g. -abc
last = null;
for (let j = 1; j < arg.length; j++) {
let key = arg[j];
if (
(key < "a" || key > "z") &&
(key < "A" || key > "Z") &&
(key < "0" || key > "9") &&
key !== "?"
) {
throw new Error(`Invalid argument: -${key}.`);
}
const alias = this.alias[key];
if (alias) {
key = alias;
}
this.args[key] = "true";
last = key;
}
continue;
}
// e.g. foo
const value = arg;
if (value.length === 0) {
last = null;
continue;
}
if (last) {
this.args[last] = value;
last = null;
} else {
this.argv.push(value);
}
} }
} }
@ -843,108 +459,22 @@ export default class Config {
assert(typeof value === "string"); assert(typeof value === "string");
if (key.indexOf(prefix) !== 0) continue; if (key.indexOf(prefix) !== 0) {
continue;
key = key.substring(prefix.length); }
key = key.replace(/_/g, "");
if (!isUpperKey(key)) { if (!isUpperKey(key)) {
continue; continue;
} }
if (value.length === 0) { key = key.substring(prefix.length);
continue; key = this.normalize(key, true);
}
key = key.toLowerCase();
// Do not allow one-letter aliases.
if (key.length > 1) {
const alias = this.alias[key];
if (alias) {
key = alias;
}
}
this.env[key] = value;
}
}
public parseQuery(query: string) {
if (typeof query !== "string") {
if (!global.location) {
return {};
}
query = global.location.search;
if (typeof query !== "string") {
return {};
}
}
return this.parseForm(query, "?", this.query);
}
public parseHash(hash: string) {
if (typeof hash !== "string") {
if (!global.location) {
return {};
}
hash = global.location.hash;
if (typeof hash !== "string") {
return {};
}
}
return this.parseForm(hash, "#", this.hash);
}
public parseForm(query: string, ch: string, map: object) {
assert(typeof query === "string");
if (query.length === 0) {
return;
}
if (query[0] === ch) {
query = query.substring(1);
}
for (const pair of query.split("&")) {
const index = pair.indexOf("=");
let key, value;
if (index !== -1) {
key = pair.substring(0, index);
value = pair.substring(index + 1);
} else {
key = pair;
value = "true";
}
key = unescape(key);
key = key.replace(/\-/g, "");
if (!isLowerKey(key)) {
continue;
}
value = unescape(value);
if (value.length === 0) { if (value.length === 0) {
continue; continue;
} }
const alias = this.alias[key]; objectPath.set(this.data, key);
if (alias) {
key = alias;
}
map[key] = value;
} }
} }
} }
@ -965,15 +495,6 @@ function fmt(key: string[] | string | number) {
return key; return key;
} }
function unescape(str: string) {
try {
str = decodeURIComponent(str);
str = str.replace(/\+/g, " ");
} catch (e) {}
str = str.replace(/\0/g, "");
return str;
}
function isAlpha(str: string) { function isAlpha(str: string) {
return /^[a-z0-9_\-]+$/i.test(str); return /^[a-z0-9_\-]+$/i.test(str);
} }
@ -982,14 +503,6 @@ function isKey(key: string) {
return /^[a-zA-Z0-9]+$/.test(key); return /^[a-zA-Z0-9]+$/.test(key);
} }
function isLowerKey(key: string) {
if (!isKey(key)) {
return false;
}
return !/[A-Z]/.test(key);
}
function isUpperKey(key: string) { function isUpperKey(key: string) {
if (!isKey(key)) { if (!isKey(key)) {
return false; return false;
@ -998,15 +511,6 @@ function isUpperKey(key: string) {
return !/[a-z]/.test(key); return !/[a-z]/.test(key);
} }
function _filter(name: string, a: object | any[], b: object | any[]) {
for (const key of Object.keys(a)) {
if (key.length > name.length && key.indexOf(name) === 0) {
const sub = key.substring(name.length);
b[sub] = a[key];
}
}
}
function fromFloat(num: number, exp: number) { function fromFloat(num: number, exp: number) {
assert(typeof num === "number" && isFinite(num)); assert(typeof num === "number" && isFinite(num));
assert(Number.isSafeInteger(exp)); assert(Number.isSafeInteger(exp));