-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.js
More file actions
180 lines (155 loc) · 4.72 KB
/
index.js
File metadata and controls
180 lines (155 loc) · 4.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"use strict";
// ===================================================================
const chokidar = require("chokidar");
const dirname = require("path").dirname;
const homedir = require("os").homedir;
const resolvePath = require("path").resolve;
const debug = require("debug")("app-conf");
const flatten = require("lodash/flatten");
const entries = require("./entries");
const merge = require("./_merge");
const pMap = require("./_pMap");
const readFile = require("./_readFile");
const UnknownFormatError = require("./unknown-format-error");
const unserialize = require("./serializers").unserialize;
// ===================================================================
const clone = (value) => JSON.parse(JSON.stringify(value));
const RELATIVE_PATH_RE = /^\.{1,2}[/\\]/;
function resolvePaths(value, base) {
if (typeof value === "string") {
return value[0] === "~" && (value[1] === "/" || value[1] === "\\")
? homedir() + value.slice(1)
: RELATIVE_PATH_RE.test(value)
? resolvePath(base, value)
: value;
}
if (value !== null && typeof value === "object") {
for (const key of Object.keys(value)) {
value[key] = resolvePaths(value[key], base);
}
return value;
}
return value;
}
// ===================================================================
function load(
appName,
{ appDir, defaults, entries: whitelist, ignoreUnknownFormats = false } = {},
) {
const useWhitelist = whitelist !== undefined;
if (useWhitelist) {
whitelist = new Set(whitelist);
}
const entryOpts = { appDir, appName };
return pMap(entries, (entry) => {
if (useWhitelist && !whitelist.has(entry.name)) {
return [];
}
const dirFn = entry.dir;
const dir = typeof dirFn === "function" ? dirFn(entryOpts) : dirFn;
return entry.list(entryOpts, dir) || [];
})
.then((files) => {
files = flatten(files);
return pMap(files, async (file) => {
try {
return await parse(file);
} catch (error) {
if (!(ignoreUnknownFormats && error instanceof UnknownFormatError)) {
throw error;
}
}
});
})
.then((data) =>
data.reduce(
(acc, cfg) => {
if (cfg !== undefined) {
merge(acc, cfg);
}
return acc;
},
defaults === undefined ? {} : clone(defaults),
),
);
}
exports.load = load;
// ===================================================================
async function parse(path) {
const file = await readFile(path);
const data = unserialize(file);
debug(file.path);
return resolvePaths(data, dirname(file.path));
}
exports.parse = parse;
// ===================================================================
const ALL_ENTRIES = entries.map((_) => _.name);
exports.watch = function watch(
{
appName,
defaults,
entries: whitelist = ALL_ENTRIES,
initialLoad = false,
...opts
},
cb,
) {
return new Promise((resolve, reject) => {
const dirs = [];
const entryOpts = { appName, appDir: opts.appDir };
entries.forEach((entry) => {
// vendor config should not change and is therefore not watched
//
// otherwise it could interfere if the program is running during
// uninstall/reinstall
if (entry.name === "vendor") {
return;
}
const dirFn = entry.dir;
const dir = typeof dirFn === "function" ? dirFn(entryOpts) : dirFn;
if (dir !== undefined) {
dirs.push(dir);
}
});
const watcher = chokidar.watch(dirs, {
depth: 0,
ignoreInitial: true,
ignorePermissionErrors: true,
});
const loadWrapper = () => {
load(appName, opts).then((config) => cb(undefined, config), cb);
};
watcher
.on("all", loadWrapper)
.once("error", reject)
.once("ready", async () => {
function unsubscribe() {
return watcher.close();
}
// vendor config is only read once and merged to defaults to avoid issues
// in case it has been deleted and another entry triggers a reload
if (whitelist.includes("vendor")) {
opts.entries = ["vendor"];
const vendor = await load(appName, opts);
opts.defaults =
defaults === undefined ? vendor : merge(clone(defaults), vendor);
opts.entries = whitelist.filter((_) => _ !== "vendor");
}
if (initialLoad) {
try {
cb(undefined, await load(appName, opts));
resolve(unsubscribe);
} catch (error) {
try {
await unsubscribe();
} catch (_) {
/* empty */
}
reject(error);
}
} else {
resolve(unsubscribe);
}
});
});
};