legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
171 lines • 6.04 kB
JavaScript
import { cosmiconfig } from 'cosmiconfig';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { DEFAULT_CONFIG, validateConfig } from './schema.js';
import { getRuntimeConfig, setRuntimeConfig } from './runtime.js';
const MODULE_NAME = 'legalmd';
// cosmiconfig search order: first match wins. package.json is checked first
// (zero-config projects), followed by dedicated rc files, then full config
// modules. The .ts variant is last to avoid requiring a ts-node environment
// in most cases.
export const SEARCH_PLACES = [
'package.json',
'.legalmdrc',
'.legalmdrc.yaml',
'.legalmdrc.json',
'legalmd.config.js',
'legalmd.config.ts',
];
let cachedConfig = null;
function isObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function deepMerge(...objects) {
const result = {};
for (const obj of objects) {
if (!obj || !isObject(obj))
continue;
for (const [key, value] of Object.entries(obj)) {
const existing = result[key];
if (isObject(existing) && isObject(value)) {
result[key] = deepMerge(existing, value);
}
else if (value !== undefined) {
result[key] = value;
}
}
}
return result;
}
/**
* Apply process.env overrides for developer convenience.
* Supports: LOG_LEVEL, DEBUG, IMAGES_DIR, STYLES_DIR, DEFAULT_INPUT_DIR,
* DEFAULT_OUTPUT_DIR, ARCHIVE_DIR, LEGAL_MD_VALIDATION_MODE, LEGAL_MD_PDF_CONNECTOR
*/
function applyEnvOverrides(config) {
if (typeof process === 'undefined' || !process.env)
return config;
const env = process.env;
const overrides = {};
// Logging
if (env.LOG_LEVEL && ['error', 'warn', 'info', 'debug'].includes(env.LOG_LEVEL)) {
overrides.logging = {
...overrides.logging,
level: env.LOG_LEVEL,
};
}
if (env.DEBUG === 'true' || env.DEBUG === '1') {
overrides.logging = { ...overrides.logging, debug: true };
}
// Paths
if (env.IMAGES_DIR)
overrides.paths = { ...overrides.paths, images: env.IMAGES_DIR };
if (env.STYLES_DIR)
overrides.paths = { ...overrides.paths, styles: env.STYLES_DIR };
if (env.DEFAULT_INPUT_DIR)
overrides.paths = { ...overrides.paths, input: env.DEFAULT_INPUT_DIR };
if (env.DEFAULT_OUTPUT_DIR)
overrides.paths = { ...overrides.paths, output: env.DEFAULT_OUTPUT_DIR };
if (env.ARCHIVE_DIR)
overrides.paths = { ...overrides.paths, archive: env.ARCHIVE_DIR };
// Processing
if (env.LEGAL_MD_VALIDATION_MODE &&
['strict', 'permissive', 'auto'].includes(env.LEGAL_MD_VALIDATION_MODE)) {
overrides.processing = {
...overrides.processing,
validationMode: env.LEGAL_MD_VALIDATION_MODE,
};
}
// PDF
if (env.LEGAL_MD_PDF_CONNECTOR &&
['auto', 'puppeteer', 'system-chrome', 'weasyprint'].includes(env.LEGAL_MD_PDF_CONNECTOR)) {
overrides.pdf = {
...overrides.pdf,
connector: env.LEGAL_MD_PDF_CONNECTOR,
};
}
// `overrides` is PartialDeep<LegalMdConfig> - a stricter subset of
// Partial<LegalMdConfig>. The cast bridges the two nominal types so deepMerge
// can accept it; the runtime shape is always valid.
return Object.keys(overrides).length > 0
? deepMerge(config, overrides)
: config;
}
/**
* Load and cache Legal Markdown configuration.
*
* Reads default config, optional global config, and project config using cosmiconfig,
* then validates and caches the merged result.
*
* @returns Promise resolving to validated runtime configuration.
* @throws {Error} When discovered configuration fails schema validation.
* @example
* ```ts
* import { loadConfig } from './config.js';
*
* const config = await loadConfig();
* console.log(config.processing.validationMode);
* ```
*/
export async function loadConfig() {
if (cachedConfig) {
return cachedConfig;
}
if (typeof process === 'undefined' || !process.versions?.node) {
return getRuntimeConfig();
}
const explorer = cosmiconfig(MODULE_NAME, {
searchPlaces: SEARCH_PLACES,
});
const globalConfigPath = path.join(os.homedir(), '.config', 'legal-md', 'config.yaml');
let globalConfig = {};
let projectConfig = {};
if (fs.existsSync(globalConfigPath)) {
const globalResult = await explorer.load(globalConfigPath);
globalConfig = (globalResult?.config ?? {});
}
const projectResult = await explorer.search(process.cwd());
projectConfig = (projectResult?.config ?? {});
// DEFAULT_CONFIG is ReadonlyDeep; globalConfig/projectConfig are PartialDeep.
// Both are structurally compatible with Partial<LegalMdConfig> but TypeScript
// requires the cast to satisfy deepMerge's generic constraint.
const merged = deepMerge(DEFAULT_CONFIG, globalConfig, projectConfig);
// Layer 4: process.env overrides (for devs: LOG_LEVEL=debug npm test)
const withEnv = applyEnvOverrides(merged);
cachedConfig = validateConfig(withEnv);
setRuntimeConfig(cachedConfig);
return cachedConfig;
}
/**
* Return the current runtime config (cached if loaded).
*
* @returns The active Legal Markdown configuration object.
* @example
* ```ts
* import { getConfig } from './config.js';
*
* const config = getConfig();
* console.log(config.paths.output);
* ```
*/
export function getConfig() {
return cachedConfig ?? getRuntimeConfig();
}
/**
* Clear loaded configuration cache and reset runtime config to defaults.
*
* @example
* ```ts
* import { clearConfigCache, loadConfig } from './config.js';
*
* clearConfigCache();
* await loadConfig();
* ```
*/
export function clearConfigCache() {
cachedConfig = null;
setRuntimeConfig(DEFAULT_CONFIG);
}
export { DEFAULT_CONFIG, validateConfig } from './schema.js';
//# sourceMappingURL=index.js.map