UNPKG

9.33 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const fs = require("fs-extra");
4const readJSON = require("load-json-file");
5const _ = require("lodash");
6const os = require("os");
7const path = require("path");
8const readPkg = require("read-pkg");
9const util_1 = require("util");
10const _pjson = require('../package.json');
11const _base = `${_pjson.name}@${_pjson.version}`;
12const debug = require('debug')('@anycli/config');
13class Config {
14 constructor() {
15 /**
16 * registers ts-node for reading typescript source (./src) instead of compiled js files (./lib)
17 * there are likely issues doing this any the tsconfig.json files are not compatible with others
18 */
19 this._base = _base;
20 this.debug = 0;
21 this.legacy = false;
22 this.arch = (os.arch() === 'ia32' ? 'x86' : os.arch());
23 this.platform = os.platform();
24 this.windows = this.platform === 'win32';
25 }
26 async load(root, pjson, baseConfig) {
27 const base = baseConfig || {};
28 this.root = root;
29 this.pjson = pjson;
30 this.name = this.pjson.name;
31 this.version = this.pjson.version;
32 if (!this.pjson.anycli) {
33 this.legacy = true;
34 this.pjson.anycli = this.pjson['cli-engine'] || {};
35 }
36 this.bin = this.pjson.anycli.bin || base.bin || this.name;
37 this.dirname = this.pjson.anycli.dirname || base.dirname || this.name;
38 this.userAgent = `${this.name}/${this.version} (${this.platform}-${this.arch}) node-${process.version}`;
39 this.shell = this._shell();
40 this.debug = this._debug();
41 this.home = process.env.HOME || (this.windows && this.windowsHome()) || os.homedir() || os.tmpdir();
42 this.cacheDir = this.scopedEnvVar('CACHE_DIR') || this.macosCacheDir() || this.dir('cache');
43 this.configDir = this.scopedEnvVar('CONFIG_DIR') || this.dir('config');
44 this.dataDir = this.scopedEnvVar('DATA_DIR') || this.dir('data');
45 this.errlog = path.join(this.cacheDir, 'error.log');
46 this.tsconfig = await this._tsConfig();
47 if (this.pjson.anycli.commands) {
48 this.commandsDir = path.join(this.root, this.pjson.anycli.commands);
49 this.commandsDirTS = await this._tsPath(this.pjson.anycli.commands);
50 }
51 this.hooks = _.mapValues(this.pjson.anycli.hooks || {}, h => _.castArray(h).map(h => path.join(this.root, h)));
52 this.hooksTS = await this._hooks();
53 if (typeof this.pjson.anycli.plugins === 'string') {
54 this.pluginsModule = path.join(this.root, this.pjson.anycli.plugins);
55 this.pluginsModuleTS = await this._tsPath(this.pjson.anycli.plugins);
56 }
57 this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.anycli.npmRegistry || 'https://registry.yarnpkg.com';
58 this.topics = topicsToArray(this.pjson.anycli.topics || {});
59 return this;
60 }
61 scopedEnvVar(k) {
62 return process.env[this.scopedEnvVarKey(k)];
63 }
64 scopedEnvVarTrue(k) {
65 let v = process.env[this.scopedEnvVarKey(k)];
66 return v === '1' || v === 'true';
67 }
68 scopedEnvVarKey(k) {
69 return [this.bin, k]
70 .map(p => p.replace(/-/g, '_'))
71 .join('_')
72 .toUpperCase();
73 }
74 _topics() {
75 }
76 dir(category) {
77 const base = process.env[`XDG_${category.toUpperCase()}_HOME`]
78 || (this.windows && process.env.LOCALAPPDATA)
79 || path.join(this.home, category === 'data' ? '.local/share' : '.' + category);
80 return path.join(base, this.dirname);
81 }
82 windowsHome() { return this.windowsHomedriveHome() || this.windowsUserprofileHome(); }
83 windowsHomedriveHome() { return (process.env.HOMEDRIVE && process.env.HOMEPATH && path.join(process.env.HOMEDRIVE, process.env.HOMEPATH)); }
84 windowsUserprofileHome() { return process.env.USERPROFILE; }
85 macosCacheDir() { return this.platform === 'darwin' && path.join(this.home, 'Library', 'Caches', this.dirname) || undefined; }
86 async _tsConfig() {
87 try {
88 // ignore if no .git as it's likely not in dev mode
89 if (!await fs.pathExists(path.join(this.root, '.git')))
90 return;
91 const tsconfigPath = path.join(this.root, 'tsconfig.json');
92 const tsconfig = await readJSON(tsconfigPath);
93 if (!tsconfig || !tsconfig.compilerOptions)
94 return;
95 return tsconfig;
96 }
97 catch (err) {
98 if (err.code !== 'ENOENT')
99 throw err;
100 }
101 }
102 /**
103 * convert a path from the compiled ./lib files to the ./src typescript source
104 * this is for developing typescript plugins/CLIs
105 * if there is a tsconfig and the original sources exist, it attempts to require ts-
106 */
107 async _tsPath(orig) {
108 if (!orig || !this.tsconfig)
109 return;
110 orig = path.join(this.root, orig);
111 let { rootDirs, outDir } = this.tsconfig.compilerOptions;
112 if (!rootDirs || !rootDirs.length || !outDir)
113 return;
114 let rootDir = rootDirs[0];
115 try {
116 // rewrite path from ./lib/foo to ./src/foo
117 const lib = path.join(this.root, outDir); // ./lib
118 const src = path.join(this.root, rootDir); // ./src
119 const relative = path.relative(lib, orig); // ./commands
120 const out = path.join(src, relative); // ./src/commands
121 // this can be a directory of commands or point to a hook file
122 // if it's a directory, we check if the path exists. If so, return the path to the directory.
123 // For hooks, it might point to a module, not a file. Something like "./hooks/myhook"
124 // That file doesn't exist, and the real file is "./hooks/myhook.ts"
125 // In that case we attempt to resolve to the filename. If it fails it will revert back to the lib path
126 if (await fs.pathExists(out) || await fs.pathExists(out + '.ts'))
127 return out;
128 return out;
129 }
130 catch (err) {
131 debug(err);
132 return;
133 }
134 }
135 async _hooks() {
136 const hooks = {};
137 if (_.isEmpty(this.pjson.anycli.hooks))
138 return;
139 for (let [k, h] of Object.entries(this.pjson.anycli.hooks)) {
140 hooks[k] = [];
141 for (let m of _.castArray(h)) {
142 const ts = await this._tsPath(m);
143 if (!ts)
144 return;
145 hooks[k].push(ts);
146 }
147 }
148 return hooks;
149 }
150 _shell() {
151 let shellPath;
152 const { SHELL, COMSPEC } = process.env;
153 if (SHELL) {
154 shellPath = SHELL.split('/');
155 }
156 else if (this.windows && COMSPEC) {
157 shellPath = COMSPEC.split(/\\|\//);
158 }
159 else {
160 shellPath = ['unknown'];
161 }
162 return shellPath[shellPath.length - 1];
163 }
164 _debug() {
165 try {
166 const { enabled } = require('debug')(this.bin);
167 if (enabled)
168 return 1;
169 if (this.scopedEnvVarTrue('DEBUG'))
170 return 1;
171 return 0;
172 // tslint:disable-next-line
173 }
174 catch (err) {
175 return 0;
176 }
177 }
178}
179exports.Config = Config;
180/**
181 * find package root
182 * for packages installed into node_modules this will go up directories until
183 * it finds a node_modules directory with the plugin installed into it
184 *
185 * This is needed because of the deduping npm does
186 */
187async function findPkg(name, root) {
188 // essentially just "cd .."
189 function* up(from) {
190 while (path.dirname(from) !== from) {
191 yield from;
192 from = path.dirname(from);
193 }
194 yield from;
195 }
196 for (let next of up(root)) {
197 let cur;
198 if (name) {
199 cur = path.join(next, 'node_modules', name, 'package.json');
200 }
201 else {
202 cur = path.join(next, 'package.json');
203 }
204 if (await fs.pathExists(cur))
205 return cur;
206 }
207}
208/**
209 * returns true if config is instantiated and not ConfigOptions
210 */
211function isIConfig(o) {
212 return !!o._base;
213}
214exports.isIConfig = isIConfig;
215/**
216 * reads a plugin/CLI's config
217 */
218async function read(opts = {}) {
219 let root = opts.root || (module.parent && module.parent.parent && module.parent.parent.filename) || __dirname;
220 const pkgPath = await findPkg(opts.name, root);
221 if (!pkgPath)
222 throw new Error(`could not find package.json with ${util_1.inspect(opts)}`);
223 debug('reading plugin %s', path.dirname(pkgPath));
224 const pkg = await readPkg(pkgPath);
225 const config = new Config();
226 await config.load(path.dirname(pkgPath), pkg, opts.baseConfig);
227 return config;
228}
229exports.read = read;
230function topicsToArray(input, base) {
231 if (!input)
232 return [];
233 base = base ? `${base}:` : '';
234 if (Array.isArray(input)) {
235 return input.concat(_.flatMap(input, t => topicsToArray(t.subtopics, `${base}${t.name}`)));
236 }
237 return _.flatMap(Object.keys(input), k => {
238 return [Object.assign({}, input[k], { name: `${base}${k}` })].concat(topicsToArray(input[k].subtopics, `${base}${input[k].name}`));
239 });
240}