rc9
极简的配置读写工具
用法
安装
bash
npm i rc9
导入
js
//* cjs
const { read, write, update } = require('rc9')
//* esm
import { read, write, update } from 'rc9'
read, update, write
bash
db.username = zuixinwang
db.password = hello
db.enabled = true
js
const config = read() //* or read('.conf')
//* config = { db: { username: 'zuixinwang', password: 'hello', enabled: true } }
js
update({ 'db.enabled', false }) //* or update('.conf', { 'db.enabled': false })
//* 数组后增加数据
update({ 'modules[]', 'module-x' })
js
const config = read()
config.enabled = false
write(config) //* or write(config, '.conf')
源码
源码就一个文件
ts
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { homedir } from "node:os";
//* 用到了destr库
import destr from "destr";
//* https://github.com/hughsk/flat
//* 这个库就不单拎出来了
import flat from "flat";
//* 用到了德芙哈
import { defu } from "defu";
//* 配置文件,每一行的正则
//* 任意空格,非等号字符至少1个,任意空格,等号,任意空格,任意字符串,任意空格
//* 第一个组是key,第二个组是value
const RE_KEY_VAL = /^\s*([^\s=]+)\s*=\s*(.*)?\s*$/;
//* 换行符
const RE_LINES = /\n|\r|\r\n/;
type RC = Record<string, any>;
interface RCOptions {
name?: string;
dir?: string;
flat?: boolean;
}
//* 读写是基于目录和文件的,这是默认配置
export const defaults: RCOptions = {
name: ".conf",
dir: process.cwd(),
//* 是否对deep对象进行展开,默认不展开
flat: false,
};
//* 配置合并
function withDefaults(options?: RCOptions | string): RCOptions {
if (typeof options === "string") {
options = { name: options };
}
return { ...defaults, ...options };
}
export function parse<T extends RC = RC>(
contents: string,
options: RCOptions = {}
): T {
const config: RC = {};
//* 分行
const lines = contents.split(RE_LINES);
//* 遍历
for (const line of lines) {
const match = line.match(RE_KEY_VAL);
if (!match) {
continue;
}
//* Key
const key = match[1];
//* 排除特殊key
if (!key || key === "__proto__" || key === "constructor") {
continue;
}
//* 值进行json化
const value = destr(match[2].trim() /* val */);
//* 对数组进行支持
if (key.endsWith("[]")) {
//* 去除后面的中括号[]两个字符,即截取真正的key
const nkey = key.slice(0, Math.max(0, key.length - 2));
//* 更新数组值
config[nkey] = (config[nkey] || []).concat(value);
continue;
}
//* 更新值
config[key] = value;
}
//* 是否展开处理
return options.flat
? (config as T)
: flat.unflatten(config, { overwrite: true });
}
//* 解析文件,读取文件字符串再解析
export function parseFile<T extends RC = RC>(
path: string,
options?: RCOptions
): T {
if (!existsSync(path)) {
return {} as T;
}
return parse(readFileSync(path, "utf8"), options);
}
//* 可自定义读取哪个文件
export function read<T extends RC = RC>(options?: RCOptions | string): T {
options = withDefaults(options);
return parseFile(resolve(options.dir!, options.name!), options);
}
//* 读取当前用户目录的配置
export function readUser<T extends RC = RC>(options?: RCOptions | string): T {
options = withDefaults(options);
//* 目录指向当前用户目录
//* $XDG_DATA_HOME 定义了应存储用户特定的数据文件的基准目录。默认值是 $HOME/.local/share
options.dir = process.env.XDG_CONFIG_HOME || homedir();
return read(options);
}
//* 序列化
export function serialize<T extends RC = RC>(config: T): string {
//* 打平并JSON格式化成字符串
return Object.entries(flat.flatten<RC, RC>(config))
.map(
([key, value]) =>
`${key}=${typeof value === "string" ? value : JSON.stringify(value)}`
)
.join("\n");
}
//* 写入文件
export function write<T extends RC = RC>(
config: T,
options?: RCOptions | string
) {
options = withDefaults(options);
//* 写入时需要进行序列化
writeFileSync(resolve(options.dir!, options.name!), serialize(config), {
encoding: "utf8",
});
}
//* 与读对应,写到当前用户目录中
export function writeUser<T extends RC = RC>(
config: T,
options?: RCOptions | string
) {
options = withDefaults(options);
options.dir = process.env.XDG_CONFIG_HOME || homedir();
write(config, options);
}
//* 更新
export function update<T extends RC = RC>(
config: T,
options?: RCOptions | string
): T {
options = withDefaults(options);
//* 不打平,则要进行折叠
if (!options.flat) {
config = flat.unflatten(config, { overwrite: true });
}
//* 通过德芙进行数据合并
const newConfig = defu(config, read(options));
write(newConfig, options);
return newConfig as T;
}
//* 相似地,处理到当前用户目录
export function updateUser<T extends RC = RC>(
config: T,
options?: RCOptions | string
): T {
options = withDefaults(options);
options.dir = process.env.XDG_CONFIG_HOME || homedir();
return update(config, options);
}
js
//* 判断是否是Buffer
function isBuffer (obj) {
return obj &&
obj.constructor &&
(typeof obj.constructor.isBuffer === 'function') &&
obj.constructor.isBuffer(obj)
}
function keyIdentity (key) {
return key
}
export function flatten (target, opts) {
opts = opts || {}
//* 分隔符,默认是对象属性读取符
const delimiter = opts.delimiter || '.'
//* 最大深度
const maxDepth = opts.maxDepth
//* key的转换函数
const transformKey = opts.transformKey || keyIdentity
const output = {}
function step (object, prev, currentDepth) {
currentDepth = currentDepth || 1
Object.keys(object).forEach(function (key) {
const value = object[key]
const isarray = opts.safe && Array.isArray(value)
const type = Object.prototype.toString.call(value)
const isbuffer = isBuffer(value)
const isobject = (
type === '[object Object]' ||
type === '[object Array]'
)
//* key转换
const newKey = prev
? prev + delimiter + transformKey(key)
: transformKey(key)
if (!isarray && !isbuffer && isobject && Object.keys(value).length &&
(!opts.maxDepth || currentDepth < maxDepth)) {
//* 递归调用
return step(value, newKey, currentDepth + 1)
}
output[newKey] = value
})
}
//* 开启递归
step(target)
return output
}
export function unflatten (target, opts) {
opts = opts || {}
const delimiter = opts.delimiter || '.'
const overwrite = opts.overwrite || false
const transformKey = opts.transformKey || keyIdentity
const result = {}
const isbuffer = isBuffer(target)
if (isbuffer || Object.prototype.toString.call(target) !== '[object Object]') {
return target
}
//* safely ensure that the key is
//* an integer.
function getkey (key) {
const parsedKey = Number(key)
return (
isNaN(parsedKey) ||
key.indexOf('.') !== -1 ||
opts.object
)
? key
: parsedKey
}
function addKeys (keyPrefix, recipient, target) {
return Object.keys(target).reduce(function (result, key) {
//* key转换
result[keyPrefix + delimiter + key] = target[key]
return result
}, recipient)
}
function isEmpty (val) {
const type = Object.prototype.toString.call(val)
const isArray = type === '[object Array]'
const isObject = type === '[object Object]'
if (!val) {
return true
} else if (isArray) {
return !val.length
} else if (isObject) {
return !Object.keys(val).length
}
}
target = Object.keys(target).reduce(function (result, key) {
const type = Object.prototype.toString.call(target[key])
const isObject = (type === '[object Object]' || type === '[object Array]')
if (!isObject || isEmpty(target[key])) {
result[key] = target[key]
return result
} else {
return addKeys(
key,
result,
flatten(target[key], opts)
)
}
}, {})
Object.keys(target).forEach(function (key) {
const split = key.split(delimiter).map(transformKey)
let key1 = getkey(split.shift())
let key2 = getkey(split[0])
let recipient = result
while (key2 !== undefined) {
//* 应该也需要把 constructor 排除
if (key1 === '__proto__') {
return
}
const type = Object.prototype.toString.call(recipient[key1])
const isobject = (
type === '[object Object]' ||
type === '[object Array]'
)
//* do not write over falsey, non-undefined values if overwrite is false
if (!overwrite && !isobject && typeof recipient[key1] !== 'undefined') {
return
}
if ((overwrite && !isobject) || (!overwrite && recipient[key1] == null)) {
recipient[key1] = (
typeof key2 === 'number' &&
!opts.object
? []
: {}
)
}
recipient = recipient[key1]
if (split.length > 0) {
key1 = getkey(split.shift())
key2 = getkey(split[0])
}
}
//* unflatten again for 'messy objects'
recipient[key1] = unflatten(target[key], opts)
})
return result
}