├── .gitignore ├── .gitattributes ├── .travis.yml ├── .editorconfig ├── package.json ├── readme.md ├── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '6' 5 | - '4' 6 | - '0.12' 7 | - '0.10' 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "configstore", 3 | "version": "2.0.0", 4 | "description": "Easily load and save config without having to think about where and how", 5 | "license": "BSD-2-Clause", 6 | "repository": "yeoman/configstore", 7 | "author": { 8 | "name": "Sindre Sorhus", 9 | "email": "sindresorhus@gmail.com", 10 | "url": "sindresorhus.com" 11 | }, 12 | "engines": { 13 | "node": ">=0.10.0" 14 | }, 15 | "scripts": { 16 | "test": "xo && ava" 17 | }, 18 | "files": [ 19 | "index.js" 20 | ], 21 | "keywords": [ 22 | "config", 23 | "store", 24 | "storage", 25 | "conf", 26 | "configuration", 27 | "settings", 28 | "preferences", 29 | "json", 30 | "data", 31 | "persist", 32 | "persistent", 33 | "save" 34 | ], 35 | "dependencies": { 36 | "dot-prop": "^3.0.0", 37 | "graceful-fs": "^4.1.2", 38 | "mkdirp": "^0.5.0", 39 | "object-assign": "^4.0.1", 40 | "os-tmpdir": "^1.0.0", 41 | "osenv": "^0.1.0", 42 | "uuid": "^2.0.1", 43 | "write-file-atomic": "^1.1.2", 44 | "xdg-basedir": "^2.0.0" 45 | }, 46 | "devDependencies": { 47 | "ava": "*", 48 | "path-exists": "^2.0.0", 49 | "xo": "*" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # configstore [![Build Status](https://travis-ci.org/yeoman/configstore.svg?branch=master)](https://travis-ci.org/yeoman/configstore) 2 | 3 | > Easily load and persist config without having to think about where and how 4 | 5 | Config is stored in a JSON file located in `$XDG_CONFIG_HOME` or `~/.config`.
6 | Example: `~/.config/configstore/some-id.json` 7 | 8 | 9 | ## Usage 10 | 11 | ```js 12 | const Configstore = require('configstore'); 13 | const pkg = require('./package.json'); 14 | 15 | // create a Configstore instance with an unique ID e.g. 16 | // package name and optionally some default values 17 | const conf = new Configstore(pkg.name, {foo: 'bar'}); 18 | 19 | console.log(conf.get('foo')); 20 | //=> 'bar' 21 | 22 | conf.set('awesome', true); 23 | console.log(conf.get('awesome')); 24 | //=> true 25 | 26 | // use dot-notation to access nested properties 27 | conf.set('bar.baz', true); 28 | console.log(conf.get('bar')); 29 | //=> {baz: true} 30 | 31 | conf.delete('awesome'); 32 | console.log(conf.get('awesome')); 33 | //=> undefined 34 | ``` 35 | 36 | 37 | ## API 38 | 39 | ### Configstore(packageName, [defaults], [options]) 40 | 41 | Returns a new instance. 42 | 43 | #### packageName 44 | 45 | Type: `string` 46 | 47 | Name of your package. 48 | 49 | #### defaults 50 | 51 | Type: `Object` 52 | 53 | Default config. 54 | 55 | #### options 56 | 57 | ##### globalConfigPath 58 | 59 | Type: `boolean`
60 | Default: `false` 61 | 62 | Store the config at `$CONFIG/package-name/config.json` instead of the default `$CONFIG/configstore/package-name.json`. This is not recommended as you might end up conflicting with other tools, rendering the "without having to think" idea moot. 63 | 64 | ### Instance 65 | 66 | You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a `key` to access nested properties. 67 | 68 | ### .set(key, value) 69 | 70 | Set an item. 71 | 72 | ### .set(object) 73 | 74 | Set multiple items at once. 75 | 76 | ### .get(key) 77 | 78 | Get an item. 79 | 80 | ### .has(key) 81 | 82 | Check if an item exists. 83 | 84 | ### .delete(key) 85 | 86 | Delete an item. 87 | 88 | ### .clear() 89 | 90 | Delete all items. 91 | 92 | ### .size 93 | 94 | Get the item count. 95 | 96 | ### .path 97 | 98 | Get the path to the config file. Can be used to show the user where the config file is located or even better open it for them. 99 | 100 | ### .all 101 | 102 | Get all the config as an object or replace the current config with an object: 103 | 104 | ```js 105 | conf.all = { 106 | hello: 'world' 107 | }; 108 | ``` 109 | 110 | 111 | ## License 112 | 113 | [BSD license](http://opensource.org/licenses/bsd-license.php)
114 | Copyright Google 115 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var fs = require('graceful-fs'); 4 | var osenv = require('osenv'); 5 | var assign = require('object-assign'); 6 | var mkdirp = require('mkdirp'); 7 | var uuid = require('uuid'); 8 | var xdgBasedir = require('xdg-basedir'); 9 | var osTmpdir = require('os-tmpdir'); 10 | var writeFileAtomic = require('write-file-atomic'); 11 | var dotProp = require('dot-prop'); 12 | 13 | var user = (osenv.user() || uuid.v4()).replace(/\\/g, ''); 14 | var configDir = xdgBasedir.config || path.join(osTmpdir(), user, '.config'); 15 | var permissionError = 'You don\'t have access to this file.'; 16 | var defaultPathMode = parseInt('0700', 8); 17 | var writeFileOptions = {mode: parseInt('0600', 8)}; 18 | 19 | function Configstore(id, defaults, opts) { 20 | opts = opts || {}; 21 | 22 | var pathPrefix = opts.globalConfigPath ? 23 | path.join(id, 'config.json') : 24 | path.join('configstore', id + '.json'); 25 | 26 | this.path = opts.configPath || path.join(configDir, pathPrefix); 27 | 28 | this.all = assign({}, defaults || {}, this.all || {}); 29 | } 30 | 31 | Configstore.prototype = Object.create(Object.prototype, { 32 | all: { 33 | get: function () { 34 | try { 35 | return JSON.parse(fs.readFileSync(this.path, 'utf8')); 36 | } catch (err) { 37 | // create dir if it doesn't exist 38 | if (err.code === 'ENOENT') { 39 | mkdirp.sync(path.dirname(this.path), defaultPathMode); 40 | return {}; 41 | } 42 | 43 | // improve the message of permission errors 44 | if (err.code === 'EACCES') { 45 | err.message = err.message + '\n' + permissionError + '\n'; 46 | } 47 | 48 | // empty the file if it encounters invalid JSON 49 | if (err.name === 'SyntaxError') { 50 | writeFileAtomic.sync(this.path, '', writeFileOptions); 51 | return {}; 52 | } 53 | 54 | throw err; 55 | } 56 | }, 57 | set: function (val) { 58 | try { 59 | // make sure the folder exists as it 60 | // could have been deleted in the meantime 61 | mkdirp.sync(path.dirname(this.path), defaultPathMode); 62 | 63 | writeFileAtomic.sync(this.path, JSON.stringify(val, null, '\t'), writeFileOptions); 64 | } catch (err) { 65 | // improve the message of permission errors 66 | if (err.code === 'EACCES') { 67 | err.message = err.message + '\n' + permissionError + '\n'; 68 | } 69 | 70 | throw err; 71 | } 72 | } 73 | }, 74 | size: { 75 | get: function () { 76 | return Object.keys(this.all || {}).length; 77 | } 78 | } 79 | }); 80 | 81 | Configstore.prototype.get = function (key) { 82 | return dotProp.get(this.all, key); 83 | }; 84 | 85 | Configstore.prototype.set = function (key, val) { 86 | var config = this.all; 87 | if (arguments.length === 1) { 88 | Object.keys(key).forEach(function (k) { 89 | dotProp.set(config, k, key[k]); 90 | }); 91 | } else { 92 | dotProp.set(config, key, val); 93 | } 94 | this.all = config; 95 | }; 96 | 97 | Configstore.prototype.has = function (key) { 98 | return dotProp.has(this.all, key); 99 | }; 100 | 101 | Configstore.prototype.delete = Configstore.prototype.del = function (key) { 102 | var config = this.all; 103 | dotProp.delete(config, key); 104 | this.all = config; 105 | }; 106 | 107 | Configstore.prototype.clear = function () { 108 | this.all = {}; 109 | }; 110 | 111 | module.exports = Configstore; 112 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import {serial as test} from 'ava'; 3 | import pathExists from 'path-exists'; 4 | import Configstore from './'; 5 | 6 | const configstorePath = new Configstore('configstore-test').path; 7 | 8 | test.beforeEach(t => { 9 | fs.unlinkSync(configstorePath); 10 | t.context.conf = new Configstore('configstore-test'); 11 | }); 12 | 13 | test('.set() and .get()', t => { 14 | t.context.conf.set('foo', 'bar'); 15 | t.context.conf.set('baz.boo', true); 16 | t.is(t.context.conf.get('foo'), 'bar'); 17 | t.is(t.context.conf.get('baz.boo'), true); 18 | }); 19 | 20 | test('.set() with object and .get()', t => { 21 | t.context.conf.set({ 22 | foo1: 'bar1', 23 | foo2: 'bar2', 24 | baz: { 25 | boo: 'foo', 26 | foo: { 27 | bar: 'baz' 28 | } 29 | } 30 | }); 31 | t.is(t.context.conf.get('foo1'), 'bar1'); 32 | t.is(t.context.conf.get('foo2'), 'bar2'); 33 | t.deepEqual(t.context.conf.get('baz'), {boo: 'foo', foo: {bar: 'baz'}}); 34 | t.is(t.context.conf.get('baz.boo'), 'foo'); 35 | t.deepEqual(t.context.conf.get('baz.foo'), {bar: 'baz'}); 36 | t.is(t.context.conf.get('baz.foo.bar'), 'baz'); 37 | }); 38 | 39 | test('.has()', t => { 40 | t.context.conf.set('foo', '🦄'); 41 | t.context.conf.set('baz.boo', '🦄'); 42 | t.true(t.context.conf.has('foo')); 43 | t.true(t.context.conf.has('baz.boo')); 44 | t.false(t.context.conf.has('missing')); 45 | }); 46 | 47 | test('.del() - SOFT-DEPRECATED', t => { 48 | t.context.conf.set('foo', 'bar'); 49 | t.context.conf.del('foo'); 50 | t.not(t.context.conf.get('foo'), 'bar'); 51 | }); 52 | 53 | test('.delete()', t => { 54 | t.context.conf.set('foo', 'bar'); 55 | t.context.conf.set('baz.boo', true); 56 | t.context.conf.set('baz.foo.bar', 'baz'); 57 | t.context.conf.delete('foo'); 58 | t.not(t.context.conf.get('foo'), 'bar'); 59 | t.context.conf.delete('baz.boo'); 60 | t.not(t.context.conf.get('baz.boo'), true); 61 | t.context.conf.delete('baz.foo'); 62 | t.not(t.context.conf.get('baz.foo'), {bar: 'baz'}); 63 | t.context.conf.set('foo.bar.baz', {awesome: 'icecream'}); 64 | t.context.conf.set('foo.bar.zoo', {awesome: 'redpanda'}); 65 | t.context.conf.delete('foo.bar.baz'); 66 | t.is(t.context.conf.get('foo.bar.zoo.awesome'), 'redpanda'); 67 | }); 68 | 69 | test('.clear()', t => { 70 | t.context.conf.set('foo', 'bar'); 71 | t.context.conf.set('foo1', 'bar1'); 72 | t.context.conf.set('baz.boo', true); 73 | t.context.conf.clear(); 74 | t.is(t.context.conf.size, 0); 75 | }); 76 | 77 | test('.all', t => { 78 | t.context.conf.set('foo', 'bar'); 79 | t.context.conf.set('baz.boo', true); 80 | t.is(t.context.conf.all.foo, 'bar'); 81 | t.deepEqual(t.context.conf.all.baz, {boo: true}); 82 | }); 83 | 84 | test('.size', t => { 85 | t.context.conf.set('foo', 'bar'); 86 | t.is(t.context.conf.size, 1); 87 | }); 88 | 89 | test('.path', t => { 90 | t.context.conf.set('foo', 'bar'); 91 | t.true(pathExists.sync(t.context.conf.path)); 92 | }); 93 | 94 | test('use default value', t => { 95 | const conf = new Configstore('configstore-test', {foo: 'bar'}); 96 | t.is(conf.get('foo'), 'bar'); 97 | }); 98 | 99 | test('support global namespace path option', t => { 100 | const conf = new Configstore('configstore-test', {}, {globalConfigPath: true}); 101 | const regex = /configstore-test(\/|\\)config.json$/; 102 | t.true(regex.test(conf.path)); 103 | }); 104 | 105 | test('ensure `.all` is always an object', t => { 106 | fs.unlinkSync(configstorePath); 107 | t.notThrows(() => t.context.conf.get('foo')); 108 | }); 109 | --------------------------------------------------------------------------------