refactor: 重构配置加载功能。

This commit is contained in:
Ivan Li 2021-10-28 22:08:32 +08:00
parent a02bc282bb
commit 3ad35f3358
15 changed files with 186 additions and 169 deletions

View File

@ -1,9 +1,9 @@
import { ConfigLoader } from "../src/config-loader.js";
import { ConfigLoader } from "../src/config-loader";
const loader = new ConfigLoader();
loader
.loadConfig()
.load()
.then((config) => {
console.log("config", config);
})

140
package-lock.json generated
View File

@ -11,7 +11,6 @@
"dependencies": {
"debug": "^4.3.2",
"etcd3": "^1.1.0",
"find-up": "^6.1.0",
"js-yaml": "^4.1.0"
},
"devDependencies": {
@ -21,7 +20,7 @@
"@types/node": "^14.17.17",
"rimraf": "^3.0.2",
"ts-node": "^10.2.1",
"typescript": "^4.5.0-beta"
"typescript": "^4.4.4"
}
},
"node_modules/@cspotcode/source-map-consumer": {
@ -325,22 +324,6 @@
"cockatiel": "^1.1.1"
}
},
"node_modules/find-up": {
"version": "6.1.0",
"resolved": "https://npm.ivanli.cc/find-up/-/find-up-6.1.0.tgz",
"integrity": "sha512-aBlseiBgQ1RSiF/brMW+toDud3NHJ2Hn3pgNJLmBf2+gBwwNbfhE/Lbg2wwwoHfD3qXReOvDH4hlywQCXp4/Lw==",
"license": "MIT",
"dependencies": {
"locate-path": "^7.0.0",
"path-exists": "^5.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://npm.ivanli.cc/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -399,21 +382,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/locate-path": {
"version": "7.0.0",
"resolved": "https://npm.ivanli.cc/locate-path/-/locate-path-7.0.0.tgz",
"integrity": "sha512-+cg2yXqDUKfo4hsFxwa3G1cBJeA+gs1vD8FyV9/odWoUlQe/4syxHQ5DPtKjtfm6gnKbZzjCqzX03kXosvZB1w==",
"license": "MIT",
"dependencies": {
"p-locate": "^6.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://npm.ivanli.cc/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -462,45 +430,6 @@
"wrappy": "1"
}
},
"node_modules/p-limit": {
"version": "4.0.0",
"resolved": "https://npm.ivanli.cc/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"license": "MIT",
"dependencies": {
"yocto-queue": "^1.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "6.0.0",
"resolved": "https://npm.ivanli.cc/p-locate/-/p-locate-6.0.0.tgz",
"integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
"license": "MIT",
"dependencies": {
"p-limit": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://npm.ivanli.cc/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://npm.ivanli.cc/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -599,9 +528,9 @@
}
},
"node_modules/typescript": {
"version": "4.5.0-beta",
"resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.5.0-beta.tgz",
"integrity": "sha512-7PvWhki2lwukaR9osVhFnNzxaE4LM+gC94dlwcvS+Tqz8+U65va7FbKo02bT+/MFlnMPM8bsPUXvHiMD/Mg3Jg==",
"version": "4.4.4",
"resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -628,18 +557,6 @@
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "1.0.0",
"resolved": "https://npm.ivanli.cc/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
},
"dependencies": {
@ -870,15 +787,6 @@
"cockatiel": "^1.1.1"
}
},
"find-up": {
"version": "6.1.0",
"resolved": "https://npm.ivanli.cc/find-up/-/find-up-6.1.0.tgz",
"integrity": "sha512-aBlseiBgQ1RSiF/brMW+toDud3NHJ2Hn3pgNJLmBf2+gBwwNbfhE/Lbg2wwwoHfD3qXReOvDH4hlywQCXp4/Lw==",
"requires": {
"locate-path": "^7.0.0",
"path-exists": "^5.0.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://npm.ivanli.cc/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -923,14 +831,6 @@
"argparse": "^2.0.1"
}
},
"locate-path": {
"version": "7.0.0",
"resolved": "https://npm.ivanli.cc/locate-path/-/locate-path-7.0.0.tgz",
"integrity": "sha512-+cg2yXqDUKfo4hsFxwa3G1cBJeA+gs1vD8FyV9/odWoUlQe/4syxHQ5DPtKjtfm6gnKbZzjCqzX03kXosvZB1w==",
"requires": {
"p-locate": "^6.0.0"
}
},
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://npm.ivanli.cc/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -970,27 +870,6 @@
"wrappy": "1"
}
},
"p-limit": {
"version": "4.0.0",
"resolved": "https://npm.ivanli.cc/p-limit/-/p-limit-4.0.0.tgz",
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
"requires": {
"yocto-queue": "^1.0.0"
}
},
"p-locate": {
"version": "6.0.0",
"resolved": "https://npm.ivanli.cc/p-locate/-/p-locate-6.0.0.tgz",
"integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
"requires": {
"p-limit": "^4.0.0"
}
},
"path-exists": {
"version": "5.0.0",
"resolved": "https://npm.ivanli.cc/path-exists/-/path-exists-5.0.0.tgz",
"integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://npm.ivanli.cc/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -1047,9 +926,9 @@
}
},
"typescript": {
"version": "4.5.0-beta",
"resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.5.0-beta.tgz",
"integrity": "sha512-7PvWhki2lwukaR9osVhFnNzxaE4LM+gC94dlwcvS+Tqz8+U65va7FbKo02bT+/MFlnMPM8bsPUXvHiMD/Mg3Jg==",
"version": "4.4.4",
"resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"dev": true
},
"wrappy": {
@ -1063,11 +942,6 @@
"resolved": "https://npm.ivanli.cc/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
},
"yocto-queue": {
"version": "1.0.0",
"resolved": "https://npm.ivanli.cc/yocto-queue/-/yocto-queue-1.0.0.tgz",
"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g=="
}
}
}

View File

@ -1,16 +1,19 @@
{
"name": "configuration",
"name": "@fennec/configuration",
"version": "0.0.1",
"description": "Fennec configuration loader and share info.",
"main": "./lib/index.js",
"types": "./lib/types/index.d.ts",
"type": "module",
"main": "lib",
"types": "lib/types/index.d.ts",
"files": [
"lib/**/*",
"*.md"
],
"scripts": {
"prebuild": "rimraf lib",
"build": "tsc -p \"tsconfig.build.json\"",
"prepublishOnly": "npm run build",
"start:example-register": "DEBUG=* ts-node ./examples/register.ts",
"start:example-config-loader": "DEBUG=* node --loader ts-node/esm ./examples/config-loader.ts",
"test": "echo \"Error: no test specified\" && exit 1"
"start:example-config-loader": "DEBUG=* ts-node ./examples/config-loader.ts"
},
"repository": {
"type": "git",
@ -25,9 +28,7 @@
"license": "MIT",
"dependencies": {
"debug": "^4.3.2",
"esm": "^3.2.25",
"etcd3": "^1.1.0",
"find-up": "^6.1.0",
"js-yaml": "^4.1.0"
},
"devDependencies": {
@ -37,6 +38,6 @@
"@types/node": "^14.17.17",
"rimraf": "^3.0.2",
"ts-node": "^10.2.1",
"typescript": "^4.5.0-beta"
"typescript": "^4.4.4"
}
}

View File

@ -1,10 +1,11 @@
import { AppConfig } from "./app-config.model.js";
import { AppConfig } from "./app-config.model";
import { Etcd3 } from "etcd3";
import { load } from "js-yaml";
import { hostname } from "os";
import { getEtcdInstance } from "./etcd-connection.js";
import { getEtcdInstance } from "./etcd-connection";
import { readFile } from "fs/promises";
import debug from "debug";
import { findUp } from "./utils/find-up";
export class ConfigLoader {
private etcd: Etcd3;
@ -17,16 +18,17 @@ export class ConfigLoader {
this.etcd = getEtcdInstance(config);
}
async loadConfig<T extends Object>(configKey?: string): Promise<any> {
const config = await (await import("find-up"))
.findUp(["config,yml", "config.yaml"])
.then((path) => {
if (path) {
return readFile(path, "utf8").then((str) => load(str));
} else {
return undefined;
}
});
async load<T extends Object>(configKey?: string): Promise<any> {
const config = await findUp(
["config.yml", "config.yaml"],
process.cwd()
).then((path) => {
if (path) {
return readFile(path, "utf8").then((str) => load(str));
} else {
return undefined;
}
});
if (configKey === undefined) {
if (process.env.CONFIG_KEY) {
@ -34,16 +36,13 @@ export class ConfigLoader {
} else if (process.env.config_key) {
configKey = process.env.config_key;
} else {
configKey = await (
await import("find-up")
)
.findUp("package.json")
configKey = await findUp("package.json", process.cwd())
.then((path) => {
if (path) {
return readFile(path, "utf8");
} else {
throw new Error(
"Fallback to using package name as configuration name"
"Can't find package.json. Fallback to using package name as configuration name when CONFIG_KEY not defined."
);
}
})

View File

@ -0,0 +1 @@
export interface BaseConfiguration extends Record<string, any> {}

View File

@ -0,0 +1,43 @@
import debug from "debug";
import { Etcd3, IOptions } from "etcd3";
import { load } from "js-yaml";
import { hostname } from "os";
import { Options } from "../interfaces/options.interface";
import { getAppPkg } from "../utils/app-pkg";
import { BaseConfiguration } from "./base-configuration.interface";
const configKeyPrefix = "share/config";
const logger = debug("fennec:config:loader:etcd");
export async function readEtcdConfig<T = BaseConfiguration>({
etcd,
appName,
environment,
}: Options): Promise<T> {
if (!(etcd instanceof Etcd3)) {
etcd = new Etcd3(etcd);
}
if (appName == undefined) {
appName = await getAppPkg().then((pkg) => pkg.name);
debug.log("found appName from package.json. [%s]", appName);
}
const key = `${configKeyPrefix}/${environment ?? hostname()}/${appName}`;
debug.log('etcd key: "%s"', key);
const plainText = await etcd.get(key);
if (plainText === null) {
debug.log('No configuration found at "%s"', key);
throw new Error(`No configuration found at ${key}`);
}
try {
return (await load(plainText)) as T;
} catch (error) {
logger.log("Can not parse value as yaml");
logger.log("plainText: %s", plainText);
logger.log("%O", error);
throw new Error(`Failed to parse configuration at ${key}`);
}
}

View File

@ -0,0 +1,27 @@
import debug from "debug";
import { readFile } from "fs/promises";
import { load } from "js-yaml";
import { Options } from "../interfaces/options.interface";
import { findUp } from "../utils/find-up";
import { BaseConfiguration } from "./base-configuration.interface";
const configKeyPrefix = "share/config";
const logger = debug("fennec:config:loader:etcd");
export async function readFileConfig<
T = BaseConfiguration
>({}: Options = {}): Promise<T> {
const path = await findUp(["config.yml", "config.yaml"], process.cwd());
try {
if (path) {
return readFile(path, "utf8").then((str) => load(str) as T);
} else {
throw new Error("No config file found");
}
} catch (error) {
logger.log("Can not parse file as yaml");
logger.log("File path: %s", path);
logger.log("%O", error);
throw new Error(`Failed to parse configuration at ${path}`);
}
}

15
src/config/index.ts Normal file
View File

@ -0,0 +1,15 @@
import { Options } from "../interfaces/options.interface";
import { readEtcdConfig } from "./etcd-config-reader";
import { readFileConfig } from "./file-config-reader";
export * from "./base-configuration.interface";
export * from "./etcd-config-reader";
export * from "./file-config-reader";
export function readConfiguration(option: Options) {
try {
return readEtcdConfig(option);
} catch (e) {
return readFileConfig(option);
}
}

View File

@ -1,5 +1,5 @@
import { Etcd3 } from "etcd3";
import { AppConfig } from "./app-config.model.js";
import { AppConfig } from "./app-config.model";
function connectEtcd(config?: AppConfig) {
return new Etcd3({
...(config?.etcd ?? { hosts: ["http://rpi:2379"] }),

View File

@ -1,4 +1,5 @@
export * from "./app-config.model.js";
export * from "./config-loader.js";
export * from "./etcd-connection.js";
export * from "./service-register.js";
export * from "./app-config.model";
export * from "./config-loader";
export * from "./etcd-connection";
export * from "./service-register";
export * from "./config";

View File

@ -0,0 +1,6 @@
import { Etcd3, IOptions } from "etcd3";
export interface Options {
etcd: Etcd3 | IOptions;
appName?: string;
environment?: string;
}

View File

@ -1,7 +1,7 @@
import { AppConfig } from "./app-config.model.js";
import { AppConfig } from "./app-config.model";
import { Etcd3 } from "etcd3";
import { hostname } from "os";
import { getEtcdInstance } from "./etcd-connection.js";
import { getEtcdInstance } from "./etcd-connection";
import debug from "debug";
export class ServiceRegister {

18
src/utils/app-pkg.ts Normal file
View File

@ -0,0 +1,18 @@
import { readFile } from "fs/promises";
import { findUp } from "./find-up";
interface Pkg {
name: string;
}
export async function getAppPkg<T = Pkg>(): Promise<T> {
return await findUp("package.json", process.cwd())
.then((path) => {
if (path) {
return readFile(path, "utf8");
} else {
throw new Error("Can't find package.json.");
}
})
.then((str) => JSON.parse(str));
}

33
src/utils/find-up.ts Normal file
View File

@ -0,0 +1,33 @@
import path from "path";
import fs from "fs/promises";
export const findUp = async (
filenameList: string[] | string,
from: string
): Promise<string | null> => {
if (!Array.isArray(filenameList)) {
filenameList = [filenameList];
}
try {
const isDir = await fs.stat(from).then((stat) => stat.isDirectory());
if (!isDir) {
return null;
}
for (const filename of filenameList) {
const filepath = path.resolve(from, filename);
if (
await fs
.stat(filepath)
.then((stat) => stat.isFile() || stat.isDirectory())
) {
return filepath;
}
}
} catch (err: any) {
if (err.code === "ENOENT") {
return null;
}
throw err;
}
return findUp(filenameList, path.resolve(from, ".."));
};

View File

@ -5,6 +5,5 @@
"sourceMap": true,
"declaration": true,
"declarationDir": "./lib/types",
"module": "node12",
},
}