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(); const loader = new ConfigLoader();
loader loader
.loadConfig() .load()
.then((config) => { .then((config) => {
console.log("config", config); console.log("config", config);
}) })

140
package-lock.json generated
View File

@ -11,7 +11,6 @@
"dependencies": { "dependencies": {
"debug": "^4.3.2", "debug": "^4.3.2",
"etcd3": "^1.1.0", "etcd3": "^1.1.0",
"find-up": "^6.1.0",
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -21,7 +20,7 @@
"@types/node": "^14.17.17", "@types/node": "^14.17.17",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.5.0-beta" "typescript": "^4.4.4"
} }
}, },
"node_modules/@cspotcode/source-map-consumer": { "node_modules/@cspotcode/source-map-consumer": {
@ -325,22 +324,6 @@
"cockatiel": "^1.1.1" "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": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://npm.ivanli.cc/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://npm.ivanli.cc/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -399,21 +382,6 @@
"js-yaml": "bin/js-yaml.js" "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": { "node_modules/lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://npm.ivanli.cc/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://npm.ivanli.cc/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -462,45 +430,6 @@
"wrappy": "1" "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": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://npm.ivanli.cc/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://npm.ivanli.cc/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -599,9 +528,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.5.0-beta", "version": "4.4.4",
"resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.5.0-beta.tgz", "resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-7PvWhki2lwukaR9osVhFnNzxaE4LM+gC94dlwcvS+Tqz8+U65va7FbKo02bT+/MFlnMPM8bsPUXvHiMD/Mg3Jg==", "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@ -628,18 +557,6 @@
"engines": { "engines": {
"node": ">=6" "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": { "dependencies": {
@ -870,15 +787,6 @@
"cockatiel": "^1.1.1" "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": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://npm.ivanli.cc/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://npm.ivanli.cc/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -923,14 +831,6 @@
"argparse": "^2.0.1" "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": { "lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://npm.ivanli.cc/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://npm.ivanli.cc/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@ -970,27 +870,6 @@
"wrappy": "1" "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": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://npm.ivanli.cc/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://npm.ivanli.cc/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -1047,9 +926,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "4.5.0-beta", "version": "4.4.4",
"resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.5.0-beta.tgz", "resolved": "https://npm.ivanli.cc/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-7PvWhki2lwukaR9osVhFnNzxaE4LM+gC94dlwcvS+Tqz8+U65va7FbKo02bT+/MFlnMPM8bsPUXvHiMD/Mg3Jg==", "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"dev": true "dev": true
}, },
"wrappy": { "wrappy": {
@ -1063,11 +942,6 @@
"resolved": "https://npm.ivanli.cc/yn/-/yn-3.1.1.tgz", "resolved": "https://npm.ivanli.cc/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true "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", "version": "0.0.1",
"description": "Fennec configuration loader and share info.", "description": "Fennec configuration loader and share info.",
"main": "./lib/index.js", "main": "lib",
"types": "./lib/types/index.d.ts", "types": "lib/types/index.d.ts",
"type": "module", "files": [
"lib/**/*",
"*.md"
],
"scripts": { "scripts": {
"prebuild": "rimraf lib", "prebuild": "rimraf lib",
"build": "tsc -p \"tsconfig.build.json\"", "build": "tsc -p \"tsconfig.build.json\"",
"prepublishOnly": "npm run build",
"start:example-register": "DEBUG=* ts-node ./examples/register.ts", "start:example-register": "DEBUG=* ts-node ./examples/register.ts",
"start:example-config-loader": "DEBUG=* node --loader ts-node/esm ./examples/config-loader.ts", "start:example-config-loader": "DEBUG=* ts-node ./examples/config-loader.ts"
"test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -25,9 +28,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.2", "debug": "^4.3.2",
"esm": "^3.2.25",
"etcd3": "^1.1.0", "etcd3": "^1.1.0",
"find-up": "^6.1.0",
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -37,6 +38,6 @@
"@types/node": "^14.17.17", "@types/node": "^14.17.17",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^10.2.1", "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 { Etcd3 } from "etcd3";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { hostname } from "os"; import { hostname } from "os";
import { getEtcdInstance } from "./etcd-connection.js"; import { getEtcdInstance } from "./etcd-connection";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import debug from "debug"; import debug from "debug";
import { findUp } from "./utils/find-up";
export class ConfigLoader { export class ConfigLoader {
private etcd: Etcd3; private etcd: Etcd3;
@ -17,16 +18,17 @@ export class ConfigLoader {
this.etcd = getEtcdInstance(config); this.etcd = getEtcdInstance(config);
} }
async loadConfig<T extends Object>(configKey?: string): Promise<any> { async load<T extends Object>(configKey?: string): Promise<any> {
const config = await (await import("find-up")) const config = await findUp(
.findUp(["config,yml", "config.yaml"]) ["config.yml", "config.yaml"],
.then((path) => { process.cwd()
if (path) { ).then((path) => {
return readFile(path, "utf8").then((str) => load(str)); if (path) {
} else { return readFile(path, "utf8").then((str) => load(str));
return undefined; } else {
} return undefined;
}); }
});
if (configKey === undefined) { if (configKey === undefined) {
if (process.env.CONFIG_KEY) { if (process.env.CONFIG_KEY) {
@ -34,16 +36,13 @@ export class ConfigLoader {
} else if (process.env.config_key) { } else if (process.env.config_key) {
configKey = process.env.config_key; configKey = process.env.config_key;
} else { } else {
configKey = await ( configKey = await findUp("package.json", process.cwd())
await import("find-up")
)
.findUp("package.json")
.then((path) => { .then((path) => {
if (path) { if (path) {
return readFile(path, "utf8"); return readFile(path, "utf8");
} else { } else {
throw new Error( 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 { Etcd3 } from "etcd3";
import { AppConfig } from "./app-config.model.js"; import { AppConfig } from "./app-config.model";
function connectEtcd(config?: AppConfig) { function connectEtcd(config?: AppConfig) {
return new Etcd3({ return new Etcd3({
...(config?.etcd ?? { hosts: ["http://rpi:2379"] }), ...(config?.etcd ?? { hosts: ["http://rpi:2379"] }),

View File

@ -1,4 +1,5 @@
export * from "./app-config.model.js"; export * from "./app-config.model";
export * from "./config-loader.js"; export * from "./config-loader";
export * from "./etcd-connection.js"; export * from "./etcd-connection";
export * from "./service-register.js"; 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 { Etcd3 } from "etcd3";
import { hostname } from "os"; import { hostname } from "os";
import { getEtcdInstance } from "./etcd-connection.js"; import { getEtcdInstance } from "./etcd-connection";
import debug from "debug"; import debug from "debug";
export class ServiceRegister { 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, "sourceMap": true,
"declaration": true, "declaration": true,
"declarationDir": "./lib/types", "declarationDir": "./lib/types",
"module": "node12",
}, },
} }