Skip to content
On this page

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
}