@gutil/config 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/dist/index.d.mts +92 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +276 -0
- package/dist/index.mjs +247 -0
- package/package.json +51 -52
- package/index.d.ts +0 -2
- package/index.js +0 -5
- package/index.js.map +0 -1
- package/src/adaptor/adaptor.abstract.d.ts +0 -8
- package/src/adaptor/adaptor.abstract.js +0 -9
- package/src/adaptor/adaptor.abstract.js.map +0 -1
- package/src/adaptor/node.adaptor.d.ts +0 -6
- package/src/adaptor/node.adaptor.js +0 -60
- package/src/adaptor/node.adaptor.js.map +0 -1
- package/src/config.d.ts +0 -15
- package/src/config.exception.d.ts +0 -3
- package/src/config.exception.js +0 -9
- package/src/config.exception.js.map +0 -1
- package/src/config.js +0 -113
- package/src/config.js.map +0 -1
- package/src/interface/config.interface.d.ts +0 -15
- package/src/interface/config.interface.js +0 -3
- package/src/interface/config.interface.js.map +0 -1
- package/src/interface/request-option.interface.d.ts +0 -13
- package/src/interface/request-option.interface.js +0 -3
- package/src/interface/request-option.interface.js.map +0 -1
- package/tsconfig.tsbuildinfo +0 -5753
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @gutil/config
|
|
2
|
+
|
|
3
|
+
从配置中心(Spring Cloud Config)获取应用配置。
|
|
4
|
+
|
|
5
|
+
- 无运行时依赖,基于原生 `fetch`,要求 Node.js >= 18
|
|
6
|
+
- 同时提供 CommonJS 和 ESM,自带类型声明
|
|
7
|
+
|
|
8
|
+
## 安装
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @gutil/config
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 使用
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Config } from '@gutil/config';
|
|
18
|
+
|
|
19
|
+
const config = await Config.create({
|
|
20
|
+
baseUrl: 'https://config.example.com',
|
|
21
|
+
application: ['user', 'application'],
|
|
22
|
+
profile: 'dev',
|
|
23
|
+
label: 'master',
|
|
24
|
+
auth: {
|
|
25
|
+
username: process.env.CONFIG_USERNAME!,
|
|
26
|
+
password: process.env.CONFIG_PASSWORD!,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
config.get('eureka.instance.prefer-ip-address'); // true
|
|
31
|
+
config.get<number>('server.port', { defaultValue: 3000 }); // number
|
|
32
|
+
config.get('host', { prefix: 'eureka' }); // 等价于 get('eureka.host')
|
|
33
|
+
config.get('spring.datasource.url', { application: 'application' });
|
|
34
|
+
config.has('server.port');
|
|
35
|
+
config.all(); // 默认应用的全部配置
|
|
36
|
+
|
|
37
|
+
await config.reload(); // 重新拉取,失败时保留旧配置
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
也可以先创建实例再手动加载:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const config = new Config(options);
|
|
44
|
+
await config.load(); // 并发调用只会发一次请求
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 选项
|
|
48
|
+
|
|
49
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
50
|
+
| ------------- | ------------------------------------ | ------------ | --------------------------------------------------------------- |
|
|
51
|
+
| `baseUrl` | `string` | - | 配置中心地址 |
|
|
52
|
+
| `application` | `string \| string[]` | - | 应用名称,第一个为默认应用 |
|
|
53
|
+
| `profile` | `string` | `'default'` | 环境,多个用逗号分隔 |
|
|
54
|
+
| `label` | `string` | - | 分支或标签,支持 `release/1.0` 这类带 `/` 的名称 |
|
|
55
|
+
| `auth` | `{ username, password }` | - | Basic 认证,也可以写在 `baseUrl` 中(`https://user:pass@host`) |
|
|
56
|
+
| `headers` | `Record<string, string>` | - | 额外的请求头 |
|
|
57
|
+
| `timeout` | `number` | `10000` | 单次请求超时(毫秒) |
|
|
58
|
+
| `retries` | `number` | `0` | 网络错误或 5xx 时的重试次数 |
|
|
59
|
+
| `retryDelay` | `number` | `1000` | 重试间隔(毫秒) |
|
|
60
|
+
| `coerce` | `boolean \| (value, key) => unknown` | `true` | 是否把字符串转换为布尔、数字、`null` |
|
|
61
|
+
| `fetch` | `typeof fetch` | 全局 `fetch` | 自定义请求函数,可用于测试或代理 |
|
|
62
|
+
|
|
63
|
+
默认的类型转换规则:`"true"` / `"false"` 转为布尔值,`"null"` 转为 `null`,`"-1"`、`"1.5"` 这类十进制数字转为数字;`"007"` 和超出安全整数范围的数字保留为字符串。
|
|
64
|
+
|
|
65
|
+
同一个 key 在多个配置文件中出现时,以配置中心的优先级为准(例如 `user-dev.yml` 优先于 `user.yml`)。
|
|
66
|
+
|
|
67
|
+
所有错误都会抛出 `ConfigError`,HTTP 错误带有 `status` 字段,底层错误在 `cause` 中。
|
|
68
|
+
|
|
69
|
+
## 从 1.x 升级
|
|
70
|
+
|
|
71
|
+
| 1.x | 2.0 |
|
|
72
|
+
| ------------------------------- | --------------------------- |
|
|
73
|
+
| `applicationNames` | `application` |
|
|
74
|
+
| `branch` | `label` |
|
|
75
|
+
| `auth.userName` | `auth.username` |
|
|
76
|
+
| `get(key, { applicationName })` | `get(key, { application })` |
|
|
77
|
+
| `get(key, { keyPrefix })` | `get(key, { prefix })` |
|
|
78
|
+
| `ConfigException` | `ConfigError` |
|
|
79
|
+
|
|
80
|
+
行为变化:
|
|
81
|
+
|
|
82
|
+
- 环境配置(如 `user-dev.yml`)现在会正确覆盖通用配置(如 `user.yml`),1.x 中是反的。
|
|
83
|
+
- 支持 `.yml` 后缀的配置文件,1.x 只识别 `.yaml` 和 `.properties`。
|
|
84
|
+
- 负数、小数会转换为数字;`"007"` 和超大整数不再转换。
|
|
85
|
+
- 不再依赖 rxjs,最低 Node.js 版本为 18。
|
|
86
|
+
|
|
87
|
+
## 开发
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm install
|
|
91
|
+
npm test # 单元测试
|
|
92
|
+
npm run typecheck
|
|
93
|
+
npm run build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
连接真实配置中心的集成测试默认跳过,设置环境变量后运行:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
CONFIG_BASE_URL=https://config.example.com CONFIG_APPLICATION=application \
|
|
100
|
+
CONFIG_PROFILE=dev CONFIG_USERNAME=xxx CONFIG_PASSWORD=xxx npm test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
发布:`npm publish`(会自动执行类型检查、测试和构建)。
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
interface BasicAuth {
|
|
2
|
+
username: string;
|
|
3
|
+
password: string;
|
|
4
|
+
}
|
|
5
|
+
type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
type CoerceFn = (value: unknown, key: string) => unknown;
|
|
7
|
+
interface ConfigOptions {
|
|
8
|
+
/** 配置中心地址,例如 https://config.example.com */
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** 应用名称,多个应用时第一个为默认应用 */
|
|
11
|
+
application: string | string[];
|
|
12
|
+
/** 环境,多个环境用逗号分隔,默认 default */
|
|
13
|
+
profile?: string;
|
|
14
|
+
/** 分支 / 标签,例如 master、release/1.0 */
|
|
15
|
+
label?: string;
|
|
16
|
+
auth?: BasicAuth;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
/** 单次请求超时时间(毫秒),默认 10000 */
|
|
19
|
+
timeout?: number;
|
|
20
|
+
/** 失败重试次数,默认 0;4xx 响应不重试 */
|
|
21
|
+
retries?: number;
|
|
22
|
+
/** 重试间隔(毫秒),默认 1000 */
|
|
23
|
+
retryDelay?: number;
|
|
24
|
+
/**
|
|
25
|
+
* 配置值类型转换,默认 true:把 "true" / "false" / "null" / 数字字符串转换为对应类型。
|
|
26
|
+
* 传 false 保留原始值,或传入自定义函数。
|
|
27
|
+
*/
|
|
28
|
+
coerce?: boolean | CoerceFn;
|
|
29
|
+
/** 自定义请求函数,默认使用全局 fetch */
|
|
30
|
+
fetch?: FetchFn;
|
|
31
|
+
}
|
|
32
|
+
interface GetOptions<T> {
|
|
33
|
+
/** 应用名称,默认为第一个应用 */
|
|
34
|
+
application?: string;
|
|
35
|
+
/** key 前缀,例如 get('host', { prefix: 'eureka' }) 等价于 get('eureka.host') */
|
|
36
|
+
prefix?: string;
|
|
37
|
+
defaultValue?: T;
|
|
38
|
+
}
|
|
39
|
+
type ConfigValues = Record<string, unknown>;
|
|
40
|
+
interface PropertySource {
|
|
41
|
+
name: string;
|
|
42
|
+
source?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
interface Environment {
|
|
45
|
+
name?: string;
|
|
46
|
+
profiles?: string[];
|
|
47
|
+
label?: string | null;
|
|
48
|
+
version?: string | null;
|
|
49
|
+
propertySources?: PropertySource[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare class Config {
|
|
53
|
+
private readonly client;
|
|
54
|
+
private readonly applications;
|
|
55
|
+
private readonly profiles;
|
|
56
|
+
private readonly coerce?;
|
|
57
|
+
private data;
|
|
58
|
+
private pending;
|
|
59
|
+
/** 创建实例并完成首次加载 */
|
|
60
|
+
static create(options: ConfigOptions): Promise<Config>;
|
|
61
|
+
constructor(options: ConfigOptions);
|
|
62
|
+
get loaded(): boolean;
|
|
63
|
+
/** 加载配置,已加载时不会重复请求 */
|
|
64
|
+
load(): Promise<void>;
|
|
65
|
+
/** 重新拉取配置;失败时保留之前的配置 */
|
|
66
|
+
reload(): Promise<void>;
|
|
67
|
+
get<T = unknown>(key: string, options: GetOptions<T> & {
|
|
68
|
+
defaultValue: T;
|
|
69
|
+
}): T;
|
|
70
|
+
get<T = unknown>(key: string, options?: GetOptions<T>): T | undefined;
|
|
71
|
+
has(key: string, application?: string): boolean;
|
|
72
|
+
/** 获取某个应用的全部配置 */
|
|
73
|
+
all(application?: string): Readonly<ConfigValues>;
|
|
74
|
+
private values;
|
|
75
|
+
private refresh;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class ConfigError extends Error {
|
|
79
|
+
readonly status?: number;
|
|
80
|
+
constructor(message: string, options?: {
|
|
81
|
+
status?: number;
|
|
82
|
+
cause?: unknown;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 配置中心返回的值大多是字符串,这里转换成对应的类型。
|
|
88
|
+
* 超出安全整数范围的数字保留为字符串,避免丢失精度。
|
|
89
|
+
*/
|
|
90
|
+
declare function coerceValue(value: unknown): unknown;
|
|
91
|
+
|
|
92
|
+
export { type BasicAuth, type CoerceFn, Config, ConfigError, type ConfigOptions, type ConfigValues, type Environment, type FetchFn, type GetOptions, type PropertySource, coerceValue };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
interface BasicAuth {
|
|
2
|
+
username: string;
|
|
3
|
+
password: string;
|
|
4
|
+
}
|
|
5
|
+
type FetchFn = (input: string, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
type CoerceFn = (value: unknown, key: string) => unknown;
|
|
7
|
+
interface ConfigOptions {
|
|
8
|
+
/** 配置中心地址,例如 https://config.example.com */
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** 应用名称,多个应用时第一个为默认应用 */
|
|
11
|
+
application: string | string[];
|
|
12
|
+
/** 环境,多个环境用逗号分隔,默认 default */
|
|
13
|
+
profile?: string;
|
|
14
|
+
/** 分支 / 标签,例如 master、release/1.0 */
|
|
15
|
+
label?: string;
|
|
16
|
+
auth?: BasicAuth;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
/** 单次请求超时时间(毫秒),默认 10000 */
|
|
19
|
+
timeout?: number;
|
|
20
|
+
/** 失败重试次数,默认 0;4xx 响应不重试 */
|
|
21
|
+
retries?: number;
|
|
22
|
+
/** 重试间隔(毫秒),默认 1000 */
|
|
23
|
+
retryDelay?: number;
|
|
24
|
+
/**
|
|
25
|
+
* 配置值类型转换,默认 true:把 "true" / "false" / "null" / 数字字符串转换为对应类型。
|
|
26
|
+
* 传 false 保留原始值,或传入自定义函数。
|
|
27
|
+
*/
|
|
28
|
+
coerce?: boolean | CoerceFn;
|
|
29
|
+
/** 自定义请求函数,默认使用全局 fetch */
|
|
30
|
+
fetch?: FetchFn;
|
|
31
|
+
}
|
|
32
|
+
interface GetOptions<T> {
|
|
33
|
+
/** 应用名称,默认为第一个应用 */
|
|
34
|
+
application?: string;
|
|
35
|
+
/** key 前缀,例如 get('host', { prefix: 'eureka' }) 等价于 get('eureka.host') */
|
|
36
|
+
prefix?: string;
|
|
37
|
+
defaultValue?: T;
|
|
38
|
+
}
|
|
39
|
+
type ConfigValues = Record<string, unknown>;
|
|
40
|
+
interface PropertySource {
|
|
41
|
+
name: string;
|
|
42
|
+
source?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
interface Environment {
|
|
45
|
+
name?: string;
|
|
46
|
+
profiles?: string[];
|
|
47
|
+
label?: string | null;
|
|
48
|
+
version?: string | null;
|
|
49
|
+
propertySources?: PropertySource[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare class Config {
|
|
53
|
+
private readonly client;
|
|
54
|
+
private readonly applications;
|
|
55
|
+
private readonly profiles;
|
|
56
|
+
private readonly coerce?;
|
|
57
|
+
private data;
|
|
58
|
+
private pending;
|
|
59
|
+
/** 创建实例并完成首次加载 */
|
|
60
|
+
static create(options: ConfigOptions): Promise<Config>;
|
|
61
|
+
constructor(options: ConfigOptions);
|
|
62
|
+
get loaded(): boolean;
|
|
63
|
+
/** 加载配置,已加载时不会重复请求 */
|
|
64
|
+
load(): Promise<void>;
|
|
65
|
+
/** 重新拉取配置;失败时保留之前的配置 */
|
|
66
|
+
reload(): Promise<void>;
|
|
67
|
+
get<T = unknown>(key: string, options: GetOptions<T> & {
|
|
68
|
+
defaultValue: T;
|
|
69
|
+
}): T;
|
|
70
|
+
get<T = unknown>(key: string, options?: GetOptions<T>): T | undefined;
|
|
71
|
+
has(key: string, application?: string): boolean;
|
|
72
|
+
/** 获取某个应用的全部配置 */
|
|
73
|
+
all(application?: string): Readonly<ConfigValues>;
|
|
74
|
+
private values;
|
|
75
|
+
private refresh;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare class ConfigError extends Error {
|
|
79
|
+
readonly status?: number;
|
|
80
|
+
constructor(message: string, options?: {
|
|
81
|
+
status?: number;
|
|
82
|
+
cause?: unknown;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 配置中心返回的值大多是字符串,这里转换成对应的类型。
|
|
88
|
+
* 超出安全整数范围的数字保留为字符串,避免丢失精度。
|
|
89
|
+
*/
|
|
90
|
+
declare function coerceValue(value: unknown): unknown;
|
|
91
|
+
|
|
92
|
+
export { type BasicAuth, type CoerceFn, Config, ConfigError, type ConfigOptions, type ConfigValues, type Environment, type FetchFn, type GetOptions, type PropertySource, coerceValue };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Config: () => Config,
|
|
24
|
+
ConfigError: () => ConfigError,
|
|
25
|
+
coerceValue: () => coerceValue
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/client.ts
|
|
30
|
+
var import_promises = require("timers/promises");
|
|
31
|
+
|
|
32
|
+
// src/errors.ts
|
|
33
|
+
var ConfigError = class extends Error {
|
|
34
|
+
status;
|
|
35
|
+
constructor(message, options = {}) {
|
|
36
|
+
super(message, { cause: options.cause });
|
|
37
|
+
this.name = "ConfigError";
|
|
38
|
+
this.status = options.status;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/client.ts
|
|
43
|
+
function parseBaseUrl(raw) {
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(raw);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new ConfigError("Invalid `baseUrl`", { cause: error });
|
|
49
|
+
}
|
|
50
|
+
const auth = url.username ? {
|
|
51
|
+
username: decodeURIComponent(url.username),
|
|
52
|
+
password: decodeURIComponent(url.password)
|
|
53
|
+
} : void 0;
|
|
54
|
+
url.username = "";
|
|
55
|
+
url.password = "";
|
|
56
|
+
return { baseUrl: url.toString().replace(/\/+$/, ""), auth };
|
|
57
|
+
}
|
|
58
|
+
function buildUrl(baseUrl, applications, profiles, label) {
|
|
59
|
+
const segments = [
|
|
60
|
+
applications.map(encodeURIComponent).join(","),
|
|
61
|
+
profiles.map(encodeURIComponent).join(",")
|
|
62
|
+
];
|
|
63
|
+
if (label) segments.push(encodeURIComponent(label.replace(/\//g, "(_)")));
|
|
64
|
+
return `${baseUrl.replace(/\/+$/, "")}/${segments.join("/")}`;
|
|
65
|
+
}
|
|
66
|
+
function buildHeaders(options) {
|
|
67
|
+
const headers = {
|
|
68
|
+
Accept: "application/json",
|
|
69
|
+
...options.headers
|
|
70
|
+
};
|
|
71
|
+
const { auth } = options;
|
|
72
|
+
if (auth?.username) {
|
|
73
|
+
const token = Buffer.from(
|
|
74
|
+
`${auth.username}:${auth.password ?? ""}`
|
|
75
|
+
).toString("base64");
|
|
76
|
+
headers.Authorization = `Basic ${token}`;
|
|
77
|
+
}
|
|
78
|
+
return headers;
|
|
79
|
+
}
|
|
80
|
+
function isTimeout(error) {
|
|
81
|
+
return error?.name === "TimeoutError";
|
|
82
|
+
}
|
|
83
|
+
async function requestOnce(url, options) {
|
|
84
|
+
const timeoutMessage = `Request to ${url} timed out after ${options.timeout}ms`;
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
response = await options.fetch(url, {
|
|
88
|
+
method: "GET",
|
|
89
|
+
headers: buildHeaders(options),
|
|
90
|
+
signal: AbortSignal.timeout(options.timeout)
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = isTimeout(error) ? timeoutMessage : `Request to ${url} failed: ${error?.message ?? String(error)}`;
|
|
94
|
+
throw new ConfigError(message, { cause: error });
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
await response.body?.cancel().catch(() => void 0);
|
|
98
|
+
throw new ConfigError(
|
|
99
|
+
`Request to ${url} failed with status ${response.status}`,
|
|
100
|
+
{
|
|
101
|
+
status: response.status
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
let body;
|
|
106
|
+
try {
|
|
107
|
+
body = await response.json();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const message = isTimeout(error) ? timeoutMessage : `Invalid JSON response from ${url}`;
|
|
110
|
+
throw new ConfigError(message, { cause: error });
|
|
111
|
+
}
|
|
112
|
+
if (!body || typeof body !== "object" || body.propertySources !== void 0 && !Array.isArray(body.propertySources)) {
|
|
113
|
+
throw new ConfigError(`Unexpected response from ${url}`);
|
|
114
|
+
}
|
|
115
|
+
return body;
|
|
116
|
+
}
|
|
117
|
+
function isRetryable(error) {
|
|
118
|
+
const status = error instanceof ConfigError ? error.status : void 0;
|
|
119
|
+
return status === void 0 || status >= 500;
|
|
120
|
+
}
|
|
121
|
+
async function fetchEnvironment(options) {
|
|
122
|
+
const url = buildUrl(
|
|
123
|
+
options.baseUrl,
|
|
124
|
+
options.applications,
|
|
125
|
+
options.profiles,
|
|
126
|
+
options.label
|
|
127
|
+
);
|
|
128
|
+
for (let attempt = 0; ; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
return await requestOnce(url, options);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (attempt >= options.retries || !isRetryable(error)) throw error;
|
|
133
|
+
await (0, import_promises.setTimeout)(options.retryDelay);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/coerce.ts
|
|
139
|
+
var NUMBER_PATTERN = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
|
140
|
+
function coerceValue(value) {
|
|
141
|
+
if (typeof value !== "string") return value;
|
|
142
|
+
if (value === "true") return true;
|
|
143
|
+
if (value === "false") return false;
|
|
144
|
+
if (value === "null") return null;
|
|
145
|
+
if (NUMBER_PATTERN.test(value)) {
|
|
146
|
+
const num = Number(value);
|
|
147
|
+
if (Number.isInteger(num) && !Number.isSafeInteger(num)) return value;
|
|
148
|
+
return num;
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/parser.ts
|
|
154
|
+
function escapeRegExp(value) {
|
|
155
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
156
|
+
}
|
|
157
|
+
function matchesApplication(sourceName, application, profiles) {
|
|
158
|
+
const profilePart = profiles.length ? `(?:-(?:${profiles.map(escapeRegExp).join("|")}))?` : "";
|
|
159
|
+
const pattern = new RegExp(
|
|
160
|
+
`(?:^|[/\\\\\\[\\s'":])${escapeRegExp(application)}${profilePart}\\.(?:ya?ml|properties)(?:$|[\\]\\s'"])`
|
|
161
|
+
);
|
|
162
|
+
return pattern.test(sourceName);
|
|
163
|
+
}
|
|
164
|
+
function parseEnvironment(env, applications, profiles, coerce) {
|
|
165
|
+
const result = /* @__PURE__ */ new Map();
|
|
166
|
+
for (const application of applications) {
|
|
167
|
+
const values = /* @__PURE__ */ Object.create(null);
|
|
168
|
+
for (const { name, source } of env.propertySources ?? []) {
|
|
169
|
+
if (!source || !matchesApplication(name, application, profiles))
|
|
170
|
+
continue;
|
|
171
|
+
for (const [key, raw] of Object.entries(source)) {
|
|
172
|
+
if (Object.hasOwn(values, key)) continue;
|
|
173
|
+
values[key] = coerce ? coerce(raw, key) : raw;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
result.set(application, values);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/config.ts
|
|
182
|
+
var Config = class _Config {
|
|
183
|
+
client;
|
|
184
|
+
applications;
|
|
185
|
+
profiles;
|
|
186
|
+
coerce;
|
|
187
|
+
data = null;
|
|
188
|
+
pending = null;
|
|
189
|
+
/** 创建实例并完成首次加载 */
|
|
190
|
+
static async create(options) {
|
|
191
|
+
const config = new _Config(options);
|
|
192
|
+
await config.load();
|
|
193
|
+
return config;
|
|
194
|
+
}
|
|
195
|
+
constructor(options) {
|
|
196
|
+
const applications = [options.application].flat().filter(Boolean);
|
|
197
|
+
if (!options.baseUrl) throw new ConfigError("`baseUrl` is required");
|
|
198
|
+
if (!applications.length)
|
|
199
|
+
throw new ConfigError("`application` is required");
|
|
200
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
201
|
+
if (typeof fetchFn !== "function") {
|
|
202
|
+
throw new ConfigError(
|
|
203
|
+
"Global fetch is not available, please pass `fetch` option"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const profiles = (options.profile ?? "").split(",").map((p) => p.trim()).filter(Boolean);
|
|
207
|
+
if (!profiles.length) profiles.push("default");
|
|
208
|
+
const { baseUrl, auth: urlAuth } = parseBaseUrl(options.baseUrl);
|
|
209
|
+
this.applications = applications;
|
|
210
|
+
this.profiles = profiles;
|
|
211
|
+
this.coerce = options.coerce === false ? void 0 : typeof options.coerce === "function" ? options.coerce : coerceValue;
|
|
212
|
+
this.client = {
|
|
213
|
+
baseUrl,
|
|
214
|
+
applications,
|
|
215
|
+
profiles,
|
|
216
|
+
label: options.label,
|
|
217
|
+
auth: options.auth ?? urlAuth,
|
|
218
|
+
headers: options.headers,
|
|
219
|
+
timeout: options.timeout ?? 1e4,
|
|
220
|
+
retries: options.retries ?? 0,
|
|
221
|
+
retryDelay: options.retryDelay ?? 1e3,
|
|
222
|
+
fetch: fetchFn
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
get loaded() {
|
|
226
|
+
return this.data !== null;
|
|
227
|
+
}
|
|
228
|
+
/** 加载配置,已加载时不会重复请求 */
|
|
229
|
+
load() {
|
|
230
|
+
if (this.data) return Promise.resolve();
|
|
231
|
+
return this.refresh();
|
|
232
|
+
}
|
|
233
|
+
/** 重新拉取配置;失败时保留之前的配置 */
|
|
234
|
+
reload() {
|
|
235
|
+
return this.refresh();
|
|
236
|
+
}
|
|
237
|
+
get(key, options = {}) {
|
|
238
|
+
const values = this.values(options.application);
|
|
239
|
+
const fullKey = options.prefix ? `${options.prefix}.${key}` : key;
|
|
240
|
+
return Object.hasOwn(values, fullKey) ? values[fullKey] : options.defaultValue;
|
|
241
|
+
}
|
|
242
|
+
has(key, application) {
|
|
243
|
+
return Object.hasOwn(this.values(application), key);
|
|
244
|
+
}
|
|
245
|
+
/** 获取某个应用的全部配置 */
|
|
246
|
+
all(application) {
|
|
247
|
+
return { ...this.values(application) };
|
|
248
|
+
}
|
|
249
|
+
values(application = this.applications[0]) {
|
|
250
|
+
if (!this.data)
|
|
251
|
+
throw new ConfigError("Config is not loaded, call `load()` first");
|
|
252
|
+
const values = this.data.get(application);
|
|
253
|
+
if (!values)
|
|
254
|
+
throw new ConfigError(`Unknown application "${application}"`);
|
|
255
|
+
return values;
|
|
256
|
+
}
|
|
257
|
+
refresh() {
|
|
258
|
+
this.pending ??= fetchEnvironment(this.client).then((env) => {
|
|
259
|
+
this.data = parseEnvironment(
|
|
260
|
+
env,
|
|
261
|
+
this.applications,
|
|
262
|
+
this.profiles,
|
|
263
|
+
this.coerce
|
|
264
|
+
);
|
|
265
|
+
}).finally(() => {
|
|
266
|
+
this.pending = null;
|
|
267
|
+
});
|
|
268
|
+
return this.pending;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
272
|
+
0 && (module.exports = {
|
|
273
|
+
Config,
|
|
274
|
+
ConfigError,
|
|
275
|
+
coerceValue
|
|
276
|
+
});
|