@guanghechen/config 1.0.0-alpha.1
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/CHANGELOG.md +11 -0
- package/lib/cjs/index.cjs +118 -0
- package/lib/esm/index.mjs +104 -0
- package/lib/types/index.d.ts +53 -0
- package/package.json +56 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
# 1.0.0-alpha.1 (2023-11-07)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* ✨ add @guanghechen/config ([f982c65](https://github.com/guanghechen/sora/commit/f982c650b09cafe19311ba24bdd6a31af30b2fe2))
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var byte = require('@guanghechen/byte');
|
|
4
|
+
require('node:fs');
|
|
5
|
+
require('node:fs/promises');
|
|
6
|
+
require('node:path');
|
|
7
|
+
var mac = require('@guanghechen/mac');
|
|
8
|
+
var semver = require('semver');
|
|
9
|
+
var config_types = require('@guanghechen/config.types');
|
|
10
|
+
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var semver__default = /*#__PURE__*/_interopDefault(semver);
|
|
14
|
+
|
|
15
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
16
|
+
const prefix = 'Invariant failed';
|
|
17
|
+
function invariant(condition, message) {
|
|
18
|
+
if (condition)
|
|
19
|
+
return;
|
|
20
|
+
if (isProduction)
|
|
21
|
+
throw new Error(prefix);
|
|
22
|
+
if (message == null)
|
|
23
|
+
throw new Error(prefix + ': ');
|
|
24
|
+
throw new Error(prefix + ': ' + (message instanceof Function ? message() : message));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const clazz = 'BaseConfigKeeper';
|
|
28
|
+
class BaseConfigKeeper {
|
|
29
|
+
hashAlgorithm;
|
|
30
|
+
_resource;
|
|
31
|
+
_instance;
|
|
32
|
+
_nonce;
|
|
33
|
+
constructor(props) {
|
|
34
|
+
this.hashAlgorithm = props.hashAlgorithm ?? 'sha256';
|
|
35
|
+
this._resource = props.resource;
|
|
36
|
+
this._instance = undefined;
|
|
37
|
+
}
|
|
38
|
+
nonce(oldNonce) {
|
|
39
|
+
return oldNonce ?? byte.bytes2text(byte.randomBytes(20), 'hex');
|
|
40
|
+
}
|
|
41
|
+
get data() {
|
|
42
|
+
return this._instance;
|
|
43
|
+
}
|
|
44
|
+
compatible(version) {
|
|
45
|
+
return semver__default.default.satisfies(version, this.__compatible_version__, {
|
|
46
|
+
loose: false,
|
|
47
|
+
includePrerelease: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async update(instance) {
|
|
51
|
+
this._instance = instance;
|
|
52
|
+
}
|
|
53
|
+
async load(resource = this._resource) {
|
|
54
|
+
const configContent = await resource.load();
|
|
55
|
+
invariant(configContent !== undefined, `[${clazz}.load] Failed to load config.`);
|
|
56
|
+
const config = await this.decode(configContent);
|
|
57
|
+
const { __version__, __mac__, __nonce__, data } = config ?? {};
|
|
58
|
+
invariant(typeof __version__ === 'string' && typeof __mac__ === 'string', () => `[${clazz}.load] Bad config, invalid fields. (${JSON.stringify(config)})`);
|
|
59
|
+
invariant(this.compatible(__version__), `[${clazz}.load] Version not compatible. expect(${this.__compatible_version__}), received(${__version__})`);
|
|
60
|
+
const content = this.stringify(data);
|
|
61
|
+
const mac$1 = byte.bytes2text(mac.calcMac([byte.text2bytes(content, 'utf8')], this.hashAlgorithm), 'hex');
|
|
62
|
+
invariant(mac$1 === config.__mac__, () => `[${clazz}.load] Bad config, mac is not matched.`);
|
|
63
|
+
const instance = await this.deserialize(data);
|
|
64
|
+
this._instance = instance;
|
|
65
|
+
this._nonce = __nonce__;
|
|
66
|
+
}
|
|
67
|
+
async save(resource = this._resource) {
|
|
68
|
+
invariant(this._instance !== undefined, `[${clazz}.save] No valid data holding.`);
|
|
69
|
+
const data = await this.serialize(this._instance);
|
|
70
|
+
const content = this.stringify(data);
|
|
71
|
+
const __mac__ = byte.bytes2text(mac.calcMac([byte.text2bytes(content, 'utf8')], this.hashAlgorithm), 'hex');
|
|
72
|
+
const __nonce__ = this.nonce(this._nonce);
|
|
73
|
+
const config = {
|
|
74
|
+
__version__: this.__version__,
|
|
75
|
+
__mac__,
|
|
76
|
+
__nonce__,
|
|
77
|
+
data,
|
|
78
|
+
};
|
|
79
|
+
const stringifiedConfig = await this.encode(config);
|
|
80
|
+
await resource.save(stringifiedConfig);
|
|
81
|
+
}
|
|
82
|
+
async destroy() {
|
|
83
|
+
await this._resource.destroy();
|
|
84
|
+
this._instance = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class JsonConfigKeeper extends BaseConfigKeeper {
|
|
89
|
+
stringify(data) {
|
|
90
|
+
return JSON.stringify(data);
|
|
91
|
+
}
|
|
92
|
+
async encode(config) {
|
|
93
|
+
return JSON.stringify(config, null, 2);
|
|
94
|
+
}
|
|
95
|
+
async decode(stringifiedConfig) {
|
|
96
|
+
return JSON.parse(stringifiedConfig);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
class PlainJsonConfigKeeper extends JsonConfigKeeper {
|
|
100
|
+
__version__ = '2.0.0';
|
|
101
|
+
__compatible_version__ = '~2.0.0';
|
|
102
|
+
async serialize(instance) {
|
|
103
|
+
return instance;
|
|
104
|
+
}
|
|
105
|
+
async deserialize(data) {
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
exports.BaseConfigKeeper = BaseConfigKeeper;
|
|
111
|
+
exports.JsonConfigKeeper = JsonConfigKeeper;
|
|
112
|
+
exports.PlainJsonConfigKeeper = PlainJsonConfigKeeper;
|
|
113
|
+
Object.keys(config_types).forEach(function (k) {
|
|
114
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
115
|
+
enumerable: true,
|
|
116
|
+
get: function () { return config_types[k]; }
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { bytes2text, randomBytes, text2bytes } from '@guanghechen/byte';
|
|
2
|
+
import 'node:fs';
|
|
3
|
+
import 'node:fs/promises';
|
|
4
|
+
import 'node:path';
|
|
5
|
+
import { calcMac } from '@guanghechen/mac';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
export * from '@guanghechen/config.types';
|
|
8
|
+
|
|
9
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
10
|
+
const prefix = 'Invariant failed';
|
|
11
|
+
function invariant(condition, message) {
|
|
12
|
+
if (condition)
|
|
13
|
+
return;
|
|
14
|
+
if (isProduction)
|
|
15
|
+
throw new Error(prefix);
|
|
16
|
+
if (message == null)
|
|
17
|
+
throw new Error(prefix + ': ');
|
|
18
|
+
throw new Error(prefix + ': ' + (message instanceof Function ? message() : message));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const clazz = 'BaseConfigKeeper';
|
|
22
|
+
class BaseConfigKeeper {
|
|
23
|
+
hashAlgorithm;
|
|
24
|
+
_resource;
|
|
25
|
+
_instance;
|
|
26
|
+
_nonce;
|
|
27
|
+
constructor(props) {
|
|
28
|
+
this.hashAlgorithm = props.hashAlgorithm ?? 'sha256';
|
|
29
|
+
this._resource = props.resource;
|
|
30
|
+
this._instance = undefined;
|
|
31
|
+
}
|
|
32
|
+
nonce(oldNonce) {
|
|
33
|
+
return oldNonce ?? bytes2text(randomBytes(20), 'hex');
|
|
34
|
+
}
|
|
35
|
+
get data() {
|
|
36
|
+
return this._instance;
|
|
37
|
+
}
|
|
38
|
+
compatible(version) {
|
|
39
|
+
return semver.satisfies(version, this.__compatible_version__, {
|
|
40
|
+
loose: false,
|
|
41
|
+
includePrerelease: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async update(instance) {
|
|
45
|
+
this._instance = instance;
|
|
46
|
+
}
|
|
47
|
+
async load(resource = this._resource) {
|
|
48
|
+
const configContent = await resource.load();
|
|
49
|
+
invariant(configContent !== undefined, `[${clazz}.load] Failed to load config.`);
|
|
50
|
+
const config = await this.decode(configContent);
|
|
51
|
+
const { __version__, __mac__, __nonce__, data } = config ?? {};
|
|
52
|
+
invariant(typeof __version__ === 'string' && typeof __mac__ === 'string', () => `[${clazz}.load] Bad config, invalid fields. (${JSON.stringify(config)})`);
|
|
53
|
+
invariant(this.compatible(__version__), `[${clazz}.load] Version not compatible. expect(${this.__compatible_version__}), received(${__version__})`);
|
|
54
|
+
const content = this.stringify(data);
|
|
55
|
+
const mac = bytes2text(calcMac([text2bytes(content, 'utf8')], this.hashAlgorithm), 'hex');
|
|
56
|
+
invariant(mac === config.__mac__, () => `[${clazz}.load] Bad config, mac is not matched.`);
|
|
57
|
+
const instance = await this.deserialize(data);
|
|
58
|
+
this._instance = instance;
|
|
59
|
+
this._nonce = __nonce__;
|
|
60
|
+
}
|
|
61
|
+
async save(resource = this._resource) {
|
|
62
|
+
invariant(this._instance !== undefined, `[${clazz}.save] No valid data holding.`);
|
|
63
|
+
const data = await this.serialize(this._instance);
|
|
64
|
+
const content = this.stringify(data);
|
|
65
|
+
const __mac__ = bytes2text(calcMac([text2bytes(content, 'utf8')], this.hashAlgorithm), 'hex');
|
|
66
|
+
const __nonce__ = this.nonce(this._nonce);
|
|
67
|
+
const config = {
|
|
68
|
+
__version__: this.__version__,
|
|
69
|
+
__mac__,
|
|
70
|
+
__nonce__,
|
|
71
|
+
data,
|
|
72
|
+
};
|
|
73
|
+
const stringifiedConfig = await this.encode(config);
|
|
74
|
+
await resource.save(stringifiedConfig);
|
|
75
|
+
}
|
|
76
|
+
async destroy() {
|
|
77
|
+
await this._resource.destroy();
|
|
78
|
+
this._instance = undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class JsonConfigKeeper extends BaseConfigKeeper {
|
|
83
|
+
stringify(data) {
|
|
84
|
+
return JSON.stringify(data);
|
|
85
|
+
}
|
|
86
|
+
async encode(config) {
|
|
87
|
+
return JSON.stringify(config, null, 2);
|
|
88
|
+
}
|
|
89
|
+
async decode(stringifiedConfig) {
|
|
90
|
+
return JSON.parse(stringifiedConfig);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
class PlainJsonConfigKeeper extends JsonConfigKeeper {
|
|
94
|
+
__version__ = '2.0.0';
|
|
95
|
+
__compatible_version__ = '~2.0.0';
|
|
96
|
+
async serialize(instance) {
|
|
97
|
+
return instance;
|
|
98
|
+
}
|
|
99
|
+
async deserialize(data) {
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { BaseConfigKeeper, JsonConfigKeeper, PlainJsonConfigKeeper };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { IConfigKeeper, IConfig } from '@guanghechen/config.types';
|
|
2
|
+
export * from '@guanghechen/config.types';
|
|
3
|
+
import { IHashAlgorithm } from '@guanghechen/mac';
|
|
4
|
+
import { ITextResource } from '@guanghechen/resource.types';
|
|
5
|
+
|
|
6
|
+
interface IBaseConfigKeeperProps {
|
|
7
|
+
/**
|
|
8
|
+
* The resource which hold the config data.
|
|
9
|
+
*/
|
|
10
|
+
readonly resource: ITextResource;
|
|
11
|
+
/**
|
|
12
|
+
* The hash algorithm for generate mac of contents.
|
|
13
|
+
* @default 'sha256'
|
|
14
|
+
*/
|
|
15
|
+
readonly hashAlgorithm?: IHashAlgorithm;
|
|
16
|
+
}
|
|
17
|
+
declare abstract class BaseConfigKeeper<Instance, Data> implements IConfigKeeper<Instance> {
|
|
18
|
+
readonly hashAlgorithm: IHashAlgorithm;
|
|
19
|
+
abstract readonly __version__: string;
|
|
20
|
+
abstract readonly __compatible_version__: string;
|
|
21
|
+
protected readonly _resource: ITextResource;
|
|
22
|
+
protected _instance: Instance | undefined;
|
|
23
|
+
protected _nonce: string | undefined;
|
|
24
|
+
constructor(props: IBaseConfigKeeperProps);
|
|
25
|
+
protected abstract serialize(instance: Instance): Promise<Data>;
|
|
26
|
+
protected abstract deserialize(data: Data): Promise<Instance>;
|
|
27
|
+
protected abstract stringify(data: Data): string;
|
|
28
|
+
protected abstract encode(config: IConfig<Data>): Promise<string>;
|
|
29
|
+
protected abstract decode(stringifiedContent: string): Promise<IConfig<Data>>;
|
|
30
|
+
protected nonce(oldNonce: string | undefined): string | undefined;
|
|
31
|
+
get data(): Readonly<Instance> | undefined;
|
|
32
|
+
compatible(version: string): boolean;
|
|
33
|
+
update(instance: Instance): Promise<void>;
|
|
34
|
+
load(resource?: ITextResource): Promise<void>;
|
|
35
|
+
save(resource?: ITextResource): Promise<void>;
|
|
36
|
+
destroy(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface IJsonConfigKeeperProps extends IBaseConfigKeeperProps {
|
|
40
|
+
}
|
|
41
|
+
declare abstract class JsonConfigKeeper<Instance, Data> extends BaseConfigKeeper<Instance, Data> implements IConfigKeeper<Instance> {
|
|
42
|
+
protected stringify(data: Data): string;
|
|
43
|
+
protected encode(config: IConfig<Data>): Promise<string>;
|
|
44
|
+
protected decode(stringifiedConfig: string): Promise<IConfig<Data>>;
|
|
45
|
+
}
|
|
46
|
+
declare class PlainJsonConfigKeeper<Data> extends JsonConfigKeeper<Data, Data> implements IConfigKeeper<Data> {
|
|
47
|
+
readonly __version__: string;
|
|
48
|
+
readonly __compatible_version__: string;
|
|
49
|
+
protected serialize(instance: Data): Promise<Data>;
|
|
50
|
+
protected deserialize(data: Data): Promise<Data>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { BaseConfigKeeper, type IBaseConfigKeeperProps, type IJsonConfigKeeperProps, JsonConfigKeeper, PlainJsonConfigKeeper };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guanghechen/config",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "guanghechen",
|
|
6
|
+
"url": "https://github.com/guanghechen/"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/guanghechen/sora/tree/@guanghechen/config@1.0.0-alpha.0",
|
|
11
|
+
"directory": "packages/config"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/guanghechen/sora/tree/@guanghechen/config@1.0.0-alpha.0/packages/config#readme",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"source": "./src/index.ts",
|
|
18
|
+
"import": "./lib/esm/index.mjs",
|
|
19
|
+
"require": "./lib/cjs/index.cjs",
|
|
20
|
+
"types": "./lib/types/index.d.ts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"source": "./src/index.ts",
|
|
24
|
+
"main": "./lib/cjs/index.cjs",
|
|
25
|
+
"module": "./lib/esm/index.mjs",
|
|
26
|
+
"types": "./lib/types/index.d.ts",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">= 16.0.0"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib/",
|
|
33
|
+
"!lib/**/*.map",
|
|
34
|
+
"package.json",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "../../node_modules/.bin/rimraf lib/ && ../../node_modules/.bin/cross-env NODE_ENV=production ../../node_modules/.bin/rollup -c ../../rollup.config.mjs",
|
|
41
|
+
"prepublishOnly": "yarn build",
|
|
42
|
+
"test": "node --experimental-vm-modules ../../node_modules/.bin/jest --config ../../jest.config.mjs --rootDir ."
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@guanghechen/byte": "^1.0.0-alpha.2",
|
|
46
|
+
"@guanghechen/config.types": "^1.0.0-alpha.1",
|
|
47
|
+
"@guanghechen/mac": "^1.0.0-alpha.1",
|
|
48
|
+
"@guanghechen/resource.types": "^1.0.0-alpha.1",
|
|
49
|
+
"semver": "^7.5.4"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@guanghechen/internal": "^1.0.0-alpha.0",
|
|
53
|
+
"@types/semver": "^7.5.4"
|
|
54
|
+
},
|
|
55
|
+
"gitHead": "44cbe1dd8ebc1a7c48d9e39564717b337d3b25af"
|
|
56
|
+
}
|