后端拆分运行时配置模块
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
|
||||
export function loadEnvFiles(filePaths: string[]): void {
|
||||
for (const filePath of filePaths) {
|
||||
loadEnvFile(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
function loadEnvFile(filePath: string): void {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const rawText = fs.readFileSync(filePath, 'utf8')
|
||||
const lines = rawText.split(/\r?\n/)
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim()
|
||||
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const separatorIndex = line.indexOf('=')
|
||||
if (separatorIndex <= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = line.slice(0, separatorIndex).trim()
|
||||
if (!key || key in process.env) {
|
||||
continue
|
||||
}
|
||||
|
||||
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1))
|
||||
}
|
||||
}
|
||||
|
||||
function parseEnvValue(rawValue: string): string {
|
||||
const value = String(rawValue || '').trim()
|
||||
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const quote = value[0]
|
||||
if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
|
||||
return value.slice(1, -1)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function parseBoolean(rawValue: unknown): boolean | null {
|
||||
const normalized = String(rawValue || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function parseInteger(rawValue: unknown): number | null {
|
||||
const normalized = String(rawValue || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parsed = Number(normalized)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
export function parseJsonArray<T = unknown>(rawValue: unknown): T[] | null {
|
||||
const normalized = String(rawValue || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(normalized)
|
||||
return Array.isArray(parsed) ? parsed as T[] : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import fs from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
import { isPlainObject } from './runtime-merge.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
export function loadConfig(configPath: string): Record<string, unknown> {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const loaded = require(configPath)
|
||||
return isPlainObject(loaded) ? loaded : {}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
type PlainObject = Record<string, unknown>
|
||||
|
||||
export function deepMerge<T extends PlainObject>(baseValue: T, overrideValue: PlainObject): T {
|
||||
if (!isPlainObject(baseValue)) {
|
||||
return cloneValue(overrideValue) as T
|
||||
}
|
||||
|
||||
const result = cloneValue(baseValue) as PlainObject
|
||||
|
||||
if (!isPlainObject(overrideValue)) {
|
||||
return result as T
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(overrideValue)) {
|
||||
if (isPlainObject(value) && isPlainObject(result[key])) {
|
||||
result[key] = deepMerge(result[key] as PlainObject, value)
|
||||
continue
|
||||
}
|
||||
|
||||
result[key] = cloneValue(value)
|
||||
}
|
||||
|
||||
return result as T
|
||||
}
|
||||
|
||||
function cloneValue<T>(value: T): T {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => cloneValue(item)) as T
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
const output: PlainObject = {}
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
output[key] = cloneValue(item)
|
||||
}
|
||||
return output as T
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function isPlainObject(value: unknown): value is PlainObject {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/** @typedef {import('../types/runtime-config.js').RuntimeConfig} RuntimeConfig */
|
||||
import { loadEnvFiles, parseBoolean, parseInteger, parseJsonArray } from "./runtime-env.js";
|
||||
import { loadConfig } from "./runtime-loader.js";
|
||||
import { deepMerge } from "./runtime-merge.js";
|
||||
import type { AdminDefaultUser, RuntimeConfig } from "../types/runtime-config.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, "../..");
|
||||
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, "../..");
|
||||
@@ -19,74 +17,11 @@ loadEnvFiles([
|
||||
path.join(PROJECT_ROOT, ".env"),
|
||||
]);
|
||||
|
||||
const defaultConfig = /** @type {RuntimeConfig} */ (
|
||||
loadConfig(path.join(CONFIG_ROOT, "default.cjs"))
|
||||
);
|
||||
const defaultConfig = loadConfig(path.join(CONFIG_ROOT, "default.cjs")) as RuntimeConfig;
|
||||
|
||||
export const runtimeConfig = /** @type {RuntimeConfig} */ (
|
||||
applyEnvOverrides(defaultConfig)
|
||||
);
|
||||
export const runtimeConfig = applyEnvOverrides(defaultConfig);
|
||||
|
||||
function loadConfig(configPath) {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const loaded = require(configPath);
|
||||
return isPlainObject(loaded) ? loaded : {};
|
||||
}
|
||||
|
||||
function loadEnvFiles(filePaths) {
|
||||
for (const filePath of filePaths) {
|
||||
loadEnvFile(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawText = fs.readFileSync(filePath, "utf8");
|
||||
const lines = rawText.split(/\r?\n/);
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
|
||||
if (!line || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = line.indexOf("=");
|
||||
if (separatorIndex <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, separatorIndex).trim();
|
||||
if (!key || key in process.env) {
|
||||
continue;
|
||||
}
|
||||
|
||||
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1));
|
||||
}
|
||||
}
|
||||
|
||||
function parseEnvValue(rawValue) {
|
||||
const value = String(rawValue || "").trim();
|
||||
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const quote = value[0];
|
||||
if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function applyEnvOverrides(baseConfig) {
|
||||
function applyEnvOverrides(baseConfig: RuntimeConfig): RuntimeConfig {
|
||||
const nextConfig = deepMerge(baseConfig, {});
|
||||
|
||||
const port = parseInteger(process.env.PORT);
|
||||
@@ -192,7 +127,7 @@ function applyEnvOverrides(baseConfig) {
|
||||
nextConfig.admin.sessionTtlHours = adminSessionTtlHours;
|
||||
}
|
||||
|
||||
const adminDefaultUsers = parseJsonArray(
|
||||
const adminDefaultUsers = parseJsonArray<AdminDefaultUser>(
|
||||
process.env.ADMIN_DEFAULT_USERS_JSON
|
||||
);
|
||||
if (adminDefaultUsers) {
|
||||
@@ -582,100 +517,3 @@ function applyEnvOverrides(baseConfig) {
|
||||
|
||||
return nextConfig;
|
||||
}
|
||||
|
||||
function deepMerge(baseValue, overrideValue) {
|
||||
if (!isPlainObject(baseValue)) {
|
||||
return cloneValue(overrideValue);
|
||||
}
|
||||
|
||||
const result = cloneValue(baseValue);
|
||||
|
||||
if (!isPlainObject(overrideValue)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(overrideValue)) {
|
||||
if (isPlainObject(value) && isPlainObject(result[key])) {
|
||||
result[key] = deepMerge(result[key], value);
|
||||
continue;
|
||||
}
|
||||
|
||||
result[key] = cloneValue(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function cloneValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => cloneValue(item));
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
const output = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
output[key] = cloneValue(item);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === "[object Object]";
|
||||
}
|
||||
|
||||
function parseBoolean(rawValue) {
|
||||
const normalized = String(rawValue || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (["1", "true", "yes", "on"].includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (["0", "false", "no", "off"].includes(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseInteger(rawValue) {
|
||||
const normalized = String(rawValue || "").trim();
|
||||
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseJsonArray(rawValue) {
|
||||
const normalized = String(rawValue || "").trim();
|
||||
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(normalized);
|
||||
return Array.isArray(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBooleanLike(value) {
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return parseBoolean(value);
|
||||
}
|
||||
Reference in New Issue
Block a user