Skip to content

Commit

Permalink
Add watch option (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
yaodingyd authored and sindresorhus committed Sep 21, 2019
1 parent 997e293 commit 8bd3352
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 20 deletions.
9 changes: 9 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ declare namespace Conf {
*/
readonly accessPropertiesByDotNotation?: boolean;

/**
Watch for any changes in the config file and call the callback for `onDidChange` if set. This is useful if there are multiple processes changing the same config file.
__Currently this option doesn't work on Node.js 8 on macOS.__
@default false
*/
readonly watch?: boolean;
}
}

Expand Down
66 changes: 46 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const pkgUp = require('pkg-up');
const envPaths = require('env-paths');
const writeFileAtomic = require('write-file-atomic');
const Ajv = require('ajv');
const debounceFn = require('debounce-fn');
const semver = require('semver');
const onetime = require('onetime');

Expand Down Expand Up @@ -109,6 +110,10 @@ class Conf {
this.store = store;
}

if (options.watch) {
this._watch();
}

if (options.migrations) {
if (!options.projectVersion) {
options.projectVersion = getPackageData().version;
Expand All @@ -135,6 +140,44 @@ class Conf {
}
}

_ensureDirectory() {
// TODO: Use `fs.mkdirSync` `recursive` option when targeting Node.js 12.
// Ensure the directory exists as it could have been deleted in the meantime.
makeDir.sync(path.dirname(this.path));
}

_write(value) {
let data = this.serialize(value);

if (this.encryptionKey) {
const initializationVector = crypto.randomBytes(16);
const password = crypto.pbkdf2Sync(this.encryptionKey, initializationVector.toString(), 10000, 32, 'sha512');
const cipher = crypto.createCipheriv(encryptionAlgorithm, password, initializationVector);
data = Buffer.concat([initializationVector, Buffer.from(':'), cipher.update(Buffer.from(data)), cipher.final()]);
}

// Temporary workaround for Conf being packaged in a Ubuntu Snap app.
// See https://github.com/sindresorhus/conf/pull/82
if (process.env.SNAP) {
fs.writeFileSync(this.path, data);
} else {
writeFileAtomic.sync(this.path, data);
}
}

_watch() {
this._ensureDirectory();

if (!fs.existsSync(this.path)) {
this._write({});
}

fs.watch(this.path, {persistent: false}, debounceFn(() => {
// On Linux and Windows, writing to the config file emits a `rename` event, so we skip checking the event type.
this.events.emit('change');
}, {wait: 100}));
}

_migrate(migrations, versionToMigrate) {
let previousMigratedVersion = this._get(MIGRATION_KEY, '0.0.0');

Expand Down Expand Up @@ -353,8 +396,7 @@ class Conf {
return Object.assign(plainObject(), data);
} catch (error) {
if (error.code === 'ENOENT') {
// TODO: Use `fs.mkdirSync` `recursive` option when targeting Node.js 12
makeDir.sync(path.dirname(this.path));
this._ensureDirectory();
return plainObject();
}

Expand All @@ -367,26 +409,10 @@ class Conf {
}

set store(value) {
// Ensure the directory exists as it could have been deleted in the meantime
makeDir.sync(path.dirname(this.path));
this._ensureDirectory();

this._validate(value);
let data = this.serialize(value);

if (this.encryptionKey) {
const initializationVector = crypto.randomBytes(16);
const password = crypto.pbkdf2Sync(this.encryptionKey, initializationVector.toString(), 10000, 32, 'sha512');
const cipher = crypto.createCipheriv(encryptionAlgorithm, password, initializationVector);
data = Buffer.concat([initializationVector, Buffer.from(':'), cipher.update(Buffer.from(data)), cipher.final()]);
}

// Temporary workaround for Conf being packaged in a Ubuntu Snap app.
// See https://github.com/sindresorhus/conf/pull/82
if (process.env.SNAP) {
fs.writeFileSync(this.path, data);
} else {
writeFileAtomic.sync(this.path, data);
}
this._write(value);

this.events.emit('change');
}
Expand Down
2 changes: 2 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ new Conf<UnicornFoo>({clearInvalidConfig: false});
new Conf<UnicornFoo>({serialize: value => 'foo'});
new Conf<UnicornFoo>({deserialize: string => ({foo: 'foo', unicorn: true})});
new Conf<UnicornFoo>({projectSuffix: 'foo'});
new Conf<UnicornFoo>({watch: true});

new Conf<UnicornFoo>({
schema: {
Expand All @@ -40,6 +41,7 @@ new Conf<UnicornFoo>({
}
}
});

expectError(
new Conf<UnicornFoo>({
schema: {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
],
"dependencies": {
"ajv": "^6.10.2",
"debounce-fn": "^3.0.1",
"dot-prop": "^5.0.0",
"env-paths": "^2.2.0",
"json-schema-typed": "^7.0.1",
Expand All @@ -54,6 +55,8 @@
"ava": "^2.3.0",
"clear-module": "^4.0.0",
"del": "^5.1.0",
"delay": "^4.3.0",
"p-event": "^4.1.0",
"tempy": "^0.3.0",
"tsd": "^0.7.4",
"xo": "^0.24.0"
Expand Down
9 changes: 9 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,15 @@ console.log(config.get('foo.bar.foobar'));
//=> '🦄'
```

#### watch

type: `boolean`<br>
Default: `false`

Watch for any changes in the config file and call the callback for `onDidChange` if set. This is useful if there are multiple processes changing the same config file.

**Currently this option doesn't work on Node.js 8 on macOS.**

### Instance

You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a `key` to access nested properties.
Expand Down
60 changes: 60 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import tempy from 'tempy';
import del from 'del';
import pkgUp from 'pkg-up';
import clearModule from 'clear-module';
import pEvent from 'p-event';
import delay from 'delay';
import Conf from '.';

const fixture = '🦄';
Expand Down Expand Up @@ -717,6 +719,64 @@ test('.delete() - without dot notation', t => {
t.deepEqual(configWithoutDotNotation.get('foo.bar.zoo'), {awesome: 'redpanda'});
});

test('`watch` option watches for config file changes by another process', async t => {
if (process.platform === 'darwin' && process.version.split('.')[0] === 'v8') {
t.plan(0);
return;
}

const cwd = tempy.directory();
const conf1 = new Conf({cwd, watch: true});
const conf2 = new Conf({cwd});
conf1.set('foo', '👾');

t.plan(4);

const checkFoo = (newValue, oldValue) => {
t.is(newValue, '🐴');
t.is(oldValue, '👾');
};

t.is(conf2.get('foo'), '👾');
t.is(conf1.path, conf2.path);
conf1.onDidChange('foo', checkFoo);

(async () => {
await delay(50);
conf2.set('foo', '🐴');
})();

await pEvent(conf1.events, 'change');
});

test('`watch` option watches for config file changes by file write', async t => {
// TODO: Remove this when targeting Node.js 10.
if (process.platform === 'darwin' && process.version.split('.')[0] === 'v8') {
t.plan(0);
return;
}

const cwd = tempy.directory();
const conf = new Conf({cwd, watch: true});
conf.set('foo', '🐴');

t.plan(2);

const checkFoo = (newValue, oldValue) => {
t.is(newValue, '🦄');
t.is(oldValue, '🐴');
};

conf.onDidChange('foo', checkFoo);

(async () => {
await delay(50);
fs.writeFileSync(path.join(cwd, 'config.json'), JSON.stringify({foo: '🦄'}));
})();

await pEvent(conf.events, 'change');
});

test('migrations - should save the project version as the initial migrated version', t => {
const cwd = tempy.directory();

Expand Down

0 comments on commit 8bd3352

Please sign in to comment.