commit 069ec2a0f0a5a507a8c564fa81c0ffe4463947f2 Author: Christopher Jeffrey Date: Sun Oct 29 07:08:10 2017 -0700 bcfg: first commit. diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..0ee666f --- /dev/null +++ b/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + ["env", { + "targets": { + "browsers": ["last 2 versions"] + }, + "useBuiltins": "usage", + "loose": true + }] + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..dc2f6a3 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,98 @@ +{ + "env": { + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 8 + }, + "root": true, + "rules": { + "array-bracket-spacing": ["error", "never"], + "arrow-parens": ["error", "as-needed", { + "requireForBlockBody": true + }], + "arrow-spacing": "error", + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs"], + "camelcase": ["error", { + "properties": "never" + }], + "comma-dangle": ["error", "never"], + "consistent-return": "error", + "eol-last": ["error", "always"], + "eqeqeq": ["error", "always", { + "null": "ignore" + }], + "func-name-matching": "error", + "indent": ["off", 2, { + "SwitchCase": 1, + "CallExpression": { + "arguments": "off" + }, + "ArrayExpression": "off" + }], + "handle-callback-err": "off", + "linebreak-style": ["error", "unix"], + "max-len": ["error", { + "code": 80, + "ignorePattern": "function \\w+\\(", + "ignoreUrls": true + }], + "max-statements-per-line": ["error", { + "max": 1 + }], + "new-cap": ["error", { + "newIsCap": true, + "capIsNew": false + }], + "new-parens": "error", + "no-buffer-constructor": "error", + "no-console": "off", + "no-extra-semi": "off", + "no-fallthrough": "off", + "no-func-assign": "off", + "no-implicit-coercion": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": ["error", { + "max": 1 + }], + "no-nested-ternary": "error", + "no-param-reassign": "off", + "no-return-assign": "error", + "no-return-await": "off", + "no-shadow-restricted-names": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unused-vars": ["error", { + "vars": "all", + "args": "none", + "ignoreRestSiblings": false + }], + "no-use-before-define": ["error", { + "functions": false, + "classes": false + }], + "no-useless-escape": "off", + "no-var": "error", + "nonblock-statement-body-position": ["error", "below"], + "padded-blocks": ["error", "never"], + "prefer-arrow-callback": "error", + "prefer-const": ["error", { + "destructuring": "all", + "ignoreReadBeforeAssign": true + }], + "prefer-template": "off", + "quotes": ["error", "single"], + "semi": ["error", "always"], + "spaced-comment": ["error", "always", { + "exceptions": ["!"] + }], + "space-before-blocks": "error", + "strict": "error", + "unicode-bom": ["error", "never"], + "valid-jsdoc": "error", + "wrap-iife": ["error", "inside"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66654c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +node_modules/ +npm-debug.log +package-lock.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..9863510 --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +.git* +bench/ +docs/ +build/ +src/ +test/ +node_modules/ +binding.gyp +npm-debug.log +package-lock.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6f3897c --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all: + @npm run browserify + +browserify: + @npm run browserify + +webpack: + @npm run webpack + +clean: + @npm run clean + +lint: + @npm run lint + +test: + @npm test + +.PHONY: all browserify webpack clean lint test diff --git a/README.md b/README.md new file mode 100644 index 0000000..44ed74b --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# bcfg + +Config parser (used for bcoin). + +## Usage + +``` js +const bcfg = require('bcfg'); + +const config = bcfg('my-module', { + alias: { + 'n': 'network' + } +}); + +// Inject some custom options first. +config.inject({ + some: 'user', + options: 'here' +}); + +config.load({ + // Parse URL hash + hash: true, + // Parse querystring + query: true, + // Parse environment + env: true, + // Parse args + argv: true +}); + +// Will parse ~/.my-module/my-config.conf (throws on FS error). +config.open('my-config.conf'); + +// These will cast types and throw on incorrect type. +console.log(config.str('username')); +console.log(config.str('password')); +console.log(config.uint('userid')); +console.log(config.float('percent')); +console.log(config.bool('initialize')); +``` + +## Contribution and License Agreement + +If you contribute code to this project, you are implicitly allowing your code +to be distributed under the MIT license. You are also implicitly verifying that +all code is your original work. `` + +## License + +- Copyright (c) 2017, Christopher Jeffrey (MIT License). + +See LICENSE for more info. diff --git a/lib/bcfg.js b/lib/bcfg.js new file mode 100644 index 0000000..8032ef3 --- /dev/null +++ b/lib/bcfg.js @@ -0,0 +1,18 @@ +/*! + * bcfg.js - configuration parsing for bcoin + * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +const Config = require('./config'); + +function bcfg(module, options) { + return new Config(module, options); +} + +bcfg.config = bcfg; +bcfg.Config = Config; + +module.exports = bcfg; diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..0e0cfce --- /dev/null +++ b/lib/config.js @@ -0,0 +1,1173 @@ +/*! + * config.js - configuration parsing for bcoin + * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +const assert = require('assert'); +const Path = require('path'); +const os = require('os'); +const fs = require('./fs'); +const HOME = os.homedir ? os.homedir() : '/'; + +/** + * Config Parser + */ + +class Config { + /** + * Create a config. + * @constructor + * @param {String} module - Module name (e.g. `bcoin`). + * @param {Object?} options + */ + + constructor(module, options) { + assert(typeof module === 'string'); + assert(module.length > 0); + + this.module = module; + this.prefix = Path.join(HOME, `.${module}`); + this.suffix = null; + this.fallback = null; + this.alias = Object.create(null); + + this.options = Object.create(null); + this.data = Object.create(null); + this.env = Object.create(null); + this.args = Object.create(null); + this.argv = []; + this.pass = []; + this.query = Object.create(null); + this.hash = Object.create(null); + + if (options) + this.init(options); + } + + /** + * Initialize options. + * @private + * @param {Object} options + */ + + init(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; + } + } + } + + /** + * Inject options. + * @param {Object} options + */ + + inject(options) { + for (const key of Object.keys(options)) { + const value = options[key]; + + switch (key) { + case 'hash': + case 'query': + case 'env': + case 'argv': + case 'config': + continue; + } + + this.set(key, value); + } + } + + /** + * Load options from hash, query, env, or args. + * @param {Object} options + */ + + load(options) { + if (options.hash) + this.parseHash(options.hash); + + 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 a config file. + * @param {String} file - e.g. `bcoin.conf`. + * @throws on IO error + */ + + open(file) { + if (fs.unsupported) + return; + + 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(); + } + + /** + * Set default option. + * @param {String} key + * @param {Object} value + */ + + set(key, value) { + assert(typeof key === 'string', 'Key must be a string.'); + + if (value == null) + return; + + key = key.replace(/-/g, ''); + key = key.toLowerCase(); + + this.options[key] = value; + } + + /** + * Test whether a config option is present. + * @param {String} key + * @returns {Boolean} + */ + + has(key) { + if (typeof key === 'number') { + assert(key >= 0, 'Index must be positive.'); + if (key >= this.argv.length) + return false; + return true; + } + + assert(typeof key === 'string', 'Key must be a string.'); + + key = key.replace(/-/g, ''); + key = key.toLowerCase(); + + if (this.hash[key] != null) + return true; + + if (this.query[key] != null) + return true; + + if (this.args[key] != null) + return true; + + if (this.env[key] != null) + return true; + + if (this.data[key] != null) + return true; + + if (this.options[key] != null) + return true; + + return false; + } + + /** + * Get a config option. + * @param {String} key + * @param {Object?} fallback + * @returns {Object|null} + */ + + get(key, fallback) { + if (fallback === undefined) + fallback = null; + + if (Array.isArray(key)) { + const keys = key; + for (const key of keys) { + const value = this.get(key); + if (value !== null) + return value; + } + 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.'); + + key = key.replace(/-/g, ''); + key = key.toLowerCase(); + + if (this.hash[key] != null) + return this.hash[key]; + + if (this.query[key] != null) + return this.query[key]; + + if (this.args[key] != null) + return this.args[key]; + + if (this.env[key] != null) + return this.env[key]; + + if (this.data[key] != null) + return this.data[key]; + + if (this.options[key] != null) + return this.options[key]; + + return fallback; + } + + /** + * Get a value's type. + * @param {String} key + * @returns {String} + */ + + typeOf(key) { + const value = this.get(key); + + if (value === null) + return 'null'; + + return typeof value; + } + + /** + * Get a config option (as a string). + * @param {String} key + * @param {Object?} fallback + * @returns {String|null} + */ + + str(key, fallback) { + const value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') + throw new Error(`${fmt(key)} must be a string.`); + + return value; + } + + /** + * Get a config option (as an integer). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + + int(key, fallback) { + const value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (typeof value !== 'number') + throw new Error(`${fmt(key)} must be an int.`); + + if (!Number.isSafeInteger(value)) + throw new Error(`${fmt(key)} must be an int.`); + + return value; + } + + if (!/^\-?\d+$/.test(value)) + throw new Error(`${fmt(key)} must be an int.`); + + const num = parseInt(value, 10); + + if (!Number.isSafeInteger(num)) + throw new Error(`${fmt(key)} must be an int.`); + + return num; + } + + /** + * Get a config option (as a unsigned integer). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + + uint(key, fallback) { + const value = this.int(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (value < 0) + throw new Error(`${fmt(key)} must be a uint.`); + + return value; + } + + /** + * Get a config option (as a float). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + + float(key, fallback) { + const value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (typeof value !== 'number') + throw new Error(`${fmt(key)} must be a float.`); + + if (!isFinite(value)) + throw new Error(`${fmt(key)} must be a float.`); + + return value; + } + + if (!/^\-?\d*(?:\.\d*)?$/.test(value)) + throw new Error(`${fmt(key)} must be a float.`); + + if (!/\d/.test(value)) + throw new Error(`${fmt(key)} must be a float.`); + + const num = parseFloat(value); + + if (!isFinite(num)) + throw new Error(`${fmt(key)} must be a float.`); + + return num; + } + + /** + * Get a config option (as a positive float). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + + ufloat(key, fallback) { + const value = this.float(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (value < 0) + throw new Error(`${fmt(key)} must be a positive float.`); + + return value; + } + + /** + * Get a value (as a fixed number). + * @param {String} key + * @param {Number?} exp + * @param {Object?} fallback + * @returns {Number|null} + */ + + fixed(key, exp, fallback) { + const value = this.float(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + try { + return fromFloat(value, exp || 0); + } catch (e) { + throw new Error(`${fmt(key)} must be a fixed number.`); + } + } + + /** + * Get a value (as a positive fixed number). + * @param {String} key + * @param {Number?} exp + * @param {Object?} fallback + * @returns {Number|null} + */ + + ufixed(key, exp, fallback) { + const value = this.fixed(key, exp); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (value < 0) + throw new Error(`${fmt(key)} must be a positive fixed number.`); + + return value; + } + + /** + * Get a config option (as a boolean). + * @param {String} key + * @param {Object?} fallback + * @returns {Boolean|null} + */ + + bool(key, fallback) { + const value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + // Bitcoin Core compat. + if (typeof value === 'number') { + if (value === 1) + return true; + + if (value === 0) + return false; + } + + if (typeof value !== 'string') { + if (typeof value !== 'boolean') + throw new Error(`${fmt(key)} must be a boolean.`); + return value; + } + + if (value === 'true' || value === '1') + return true; + + if (value === 'false' || value === '0') + return false; + + throw new Error(`${fmt(key)} must be a boolean.`); + } + + /** + * Get a config option (as a buffer). + * @param {String} key + * @param {Object?} fallback + * @returns {Buffer|null} + */ + + buf(key, fallback, enc) { + const value = this.get(key); + + if (!enc) + enc = 'hex'; + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (!Buffer.isBuffer(value)) + throw new Error(`${fmt(key)} must be a buffer.`); + return value; + } + + const data = Buffer.from(value, enc); + + if (data.length !== Buffer.byteLength(value, enc)) + throw new Error(`${fmt(key)} must be a ${enc} string.`); + + return data; + } + + /** + * Get a config option (as an array of strings). + * @param {String} key + * @param {Object?} fallback + * @returns {String[]|null} + */ + + array(key, fallback) { + const value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'string') { + if (!Array.isArray(value)) + throw new Error(`${fmt(key)} must be an array.`); + return value; + } + + const parts = value.trim().split(/\s*,\s*/); + const result = []; + + for (const part of parts) { + if (part.length === 0) + continue; + + result.push(part); + } + + return result; + } + + /** + * Get a config option (as an object). + * @param {String} key + * @param {Object?} fallback + * @returns {Object|null} + */ + + obj(key, fallback) { + const value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'object' || Array.isArray(value)) + throw new Error(`${fmt(key)} must be an object.`); + + return value; + } + + /** + * Get a config option (as a function). + * @param {String} key + * @param {Object?} fallback + * @returns {Function|null} + */ + + func(key, fallback) { + const value = this.get(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + if (typeof value !== 'function') + throw new Error(`${fmt(key)} must be a function.`); + + return value; + } + + /** + * Get a config option (as a string). + * @param {String} key + * @param {Object?} fallback + * @returns {String|null} + */ + + path(key, fallback) { + let value = this.str(key); + + if (fallback === undefined) + fallback = null; + + 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); + } + + /** + * Get a config option (in MB). + * @param {String} key + * @param {Object?} fallback + * @returns {Number|null} + */ + + mb(key, fallback) { + const value = this.uint(key); + + if (fallback === undefined) + fallback = null; + + if (value === null) + return fallback; + + return value * 1024 * 1024; + } + + /** + * Grab suffix from config data. + * @returns {String} + */ + + getSuffix() { + if (!this.suffix) + throw new Error('No suffix presented.'); + + const suffix = this.str(this.suffix, this.fallback); + + assert(isAlpha(suffix), 'Bad suffix.'); + + return suffix; + } + + /** + * Grab prefix from config data. + * @private + * @returns {String} + */ + + 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); + } + + /** + * Grab config filename from config data. + * @private + * @param {String} file + * @returns {String} + */ + + getFile(file) { + const name = this.str('config'); + + if (name) + return name; + + return Path.join(this.prefix, file); + } + + /** + * Create a file path using `prefix`. + * @param {String} file + * @returns {String} + */ + + location(file) { + return Path.join(this.prefix, file); + } + + /** + * Parse config text. + * @private + * @param {String} text + */ + + parseConfig(text) { + 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; + } + } + + /** + * Parse arguments. + * @private + * @param {Array?} argv + */ + + parseArg(argv) { + 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); + } + } + } + + /** + * Parse environment variables. + * @private + * @param {Object?} env + * @returns {Object} + */ + + parseEnv(env) { + let prefix = this.module; + + prefix = prefix.toUpperCase(); + prefix = prefix.replace(/-/g, '_'); + prefix += '_'; + + if (!env || typeof env !== 'object') + env = process.env; + + assert(env && typeof env === 'object'); + + for (let key of Object.keys(env)) { + const value = env[key]; + + assert(typeof value === 'string'); + + if (key.indexOf(prefix) !== 0) + continue; + + key = key.substring(prefix.length); + key = key.replace(/_/g, ''); + + if (!isUpperKey(key)) + continue; + + if (value.length === 0) + continue; + + 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; + } + } + + /** + * Parse uri querystring variables. + * @private + * @param {String} query + */ + + 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); + } + + /** + * Parse uri hash variables. + * @private + * @param {String} hash + */ + + 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); + } + + /** + * Parse form-urlencoded variables. + * @private + * @param {String} query + */ + + parseForm(query, map) { + assert(typeof query === 'string'); + + if (query.length === 0) + return; + + let ch = '?'; + + if (map === this.hash) + ch = '#'; + + 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; + } + } +} + +/* + * Helpers + */ + +function fmt(key) { + if (Array.isArray(key)) + key = key[0]; + + if (typeof key === 'number') + return `Argument #${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) { + return /^[a-z0-9_\-]+$/i.test(str); +} + +function isKey(key) { + return /^[a-zA-Z0-9]+$/.test(key); +} + +function isLowerKey(key) { + if (!isKey(key)) + return false; + + return !/[A-Z]/.test(key); +} + +function isUpperKey(key) { + if (!isKey(key)) + return false; + + return !/[a-z]/.test(key); +} + +function fromFloat(num, exp) { + assert(typeof num === 'number' && isFinite(num)); + assert(Number.isSafeInteger(exp)); + + const str = num.toFixed(exp); + + let sign = 1; + + if (str.length > 0 && str[0] === '-') { + str = str.substring(1); + sign = -1; + } + + let hi = str; + let lo = '0'; + + const index = str.indexOf('.'); + + if (index !== -1) { + hi = str.substring(0, index); + lo = str.substring(index + 1); + } + + hi = hi.replace(/^0+/, ''); + lo = lo.replace(/0+$/, ''); + + assert(hi.length <= 16 - exp, + 'Fixed number string exceeds 2^53-1.'); + + assert(lo.length <= exp, + 'Too many decimal places in fixed number string.'); + + if (hi.length === 0) + hi = '0'; + + while (lo.length < exp) + lo += '0'; + + if (lo.length === 0) + lo = '0'; + + assert(/^\d+$/.test(hi) && /^\d+$/.test(lo), + 'Non-numeric characters in fixed number string.'); + + hi = parseInt(hi, 10); + lo = parseInt(lo, 10); + + const mult = Math.pow(10, exp); + const maxLo = Number.MAX_SAFE_INTEGER % mult; + const maxHi = (Number.MAX_SAFE_INTEGER - maxLo) / mult; + + assert(hi < maxHi || (hi === maxHi && lo <= maxLo), + 'Fixed number string exceeds 2^53-1.'); + + return sign * (hi * mult + lo); +} + +/* + * Expose + */ + +module.exports = Config; diff --git a/lib/fs-browser.js b/lib/fs-browser.js new file mode 100644 index 0000000..2122727 --- /dev/null +++ b/lib/fs-browser.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.unsupported = true; diff --git a/lib/fs.js b/lib/fs.js new file mode 100644 index 0000000..3a7e2d5 --- /dev/null +++ b/lib/fs.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('fs'); diff --git a/package.json b/package.json new file mode 100644 index 0000000..840f1b4 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "bcfg", + "version": "0.0.0", + "description": "Config parser for bcoin", + "keywords": [ + "conf", + "config" + ], + "license": "MIT", + "repository": "git://github.com/bcoin-org/bcfg.git", + "homepage": "https://github.com/bcoin-org/bcfg", + "bugs": { + "url": "https://github.com/bcoin-org/bcfg/issues" + }, + "author": "Christopher Jeffrey ", + "main": "./lib/bcfg.js", + "scripts": { + "browserify": "browserify -s bcfg lib/bcfg.js | uglifyjs -c > bcfg.js", + "clean": "rm -f bcfg.js", + "lint": "eslint lib/ test/ || exit 0", + "test": "mocha --reporter spec test/*-test.js", + "webpack": "webpack --config webpack.config.js" + }, + "devDependencies": { + "babelify": "^7.3.0", + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-preset-env": "^1.6.1", + "browserify": "^14.5.0", + "eslint": "^4.9.0", + "level-js": "^2.2.4", + "mocha": "^4.0.1", + "uglifyjs-webpack-plugin": "^1.0.0-beta.3", + "uglify-es": "^3.1.3", + "webpack": "^3.8.1" + }, + "engines": { + "node": ">=7.6.0" + }, + "browser": { + "./lib/fs": "./lib/fs-browser.js" + }, + "browserify": { + "transform": [ + "babelify" + ] + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..0753c98 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,29 @@ +'use strict'; + +const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); + +module.exports = { + target: 'web', + entry: { + 'bcfg': './lib/bcfg' + }, + output: { + library: 'bcfg', + libraryTarget: 'umd', + path: __dirname, + filename: '[name].js' + }, + resolve: { + modules: ['node_modules'], + extensions: ['-browser.js', '.js', '.json'] + }, + module: { + rules: [{ + test: /\.js$/, + loader: 'babel-loader' + }] + }, + plugins: [ + new UglifyJsPlugin() + ] +};