UNPKG

unconfig

Version:

A universal solution for loading configurations.

227 lines (221 loc) 6.35 kB
import { createRequire } from 'node:module'; import { parse, resolve, dirname, join, basename } from 'node:path'; import process from 'node:process'; import { toArray } from '@antfu/utils'; import { lstat, stat, readFile, writeFile, unlink } from '@quansync/fs'; import defu from 'defu'; import { quansync } from 'quansync/macro'; const isFile = quansync(function* (path, allowSymlinks) { try { return (yield (allowSymlinks ? lstat : stat)(path)).isFile(); } catch { return false; } }); const findUp = quansync( function* (paths, options = {}) { const { cwd = process.cwd(), stopAt = parse(cwd).root, multiple = false, allowSymlinks = true } = options; let current = cwd; const files = []; while (current && current !== stopAt) { for (const path of paths) { const filepath = resolve(current, path); if (yield isFile(filepath, allowSymlinks)) { files.push(filepath); if (!multiple) return files; } } const parent = dirname(current); if (parent === current) break; current = parent; } return files; } ); function interopDefault(mod) { if (mod == null || typeof mod !== "object" || !("default" in mod) || mod.default == null) return mod; const defaultValue = mod.default; if (typeof defaultValue !== "object") return defaultValue; for (const key in mod) { try { if (!(key in defaultValue)) { Object.defineProperty(defaultValue, key, { enumerable: key !== "default", configurable: key !== "default", get() { return mod[key]; } }); } } catch { } } return defaultValue; } const defaultExtensions = ["mts", "cts", "ts", "mjs", "cjs", "js", "json", ""]; const require = createRequire(import.meta.url); const loadConfigFile = quansync(function* (filepath, source) { let config; let parser = source.parser || "auto"; let bundleFilepath = filepath; let code; let dependencies; const read = quansync(function* () { if (code == null) code = yield readFile(filepath, "utf8"); return code; }); const importModule = quansync({ sync: () => { const { createJiti } = require("jiti"); const jiti = createJiti(import.meta.url, { fsCache: false, moduleCache: false, interopDefault: true }); config = interopDefault(jiti(bundleFilepath)); dependencies = Object.values(jiti.cache).map((i) => i.filename).filter(Boolean); }, async: async () => { const { createJiti } = await import('jiti'); const jiti = createJiti(import.meta.url, { fsCache: false, moduleCache: false, interopDefault: true }); config = interopDefault(await jiti.import(bundleFilepath, { default: true })); dependencies = Object.values(jiti.cache).map((i) => i.filename).filter(Boolean); } }); if (source.transform) { const transformed = yield source.transform(yield read(), filepath); if (transformed) { bundleFilepath = join(dirname(filepath), `__unconfig_${basename(filepath)}`); yield writeFile(bundleFilepath, transformed, "utf8"); code = transformed; } } if (parser === "auto") { try { config = JSON.parse(yield read()); parser = "json"; } catch { parser = "import"; } } try { if (!config) { if (typeof parser === "function") { config = yield parser(filepath); } else if (parser === "import") { yield importModule(); } else if (parser === "json") { config = JSON.parse(yield read()); } } if (!config) return; const rewritten = source.rewrite ? yield source.rewrite(config, filepath) : config; if (!rewritten) return void 0; return { config: rewritten, sources: [filepath], dependencies }; } catch (e) { if (source.skipOnError) return; throw e; } finally { if (bundleFilepath !== filepath) { try { yield unlink(bundleFilepath); } catch { } } } }); function createConfigLoader(options) { const sources = toArray(options.sources || []); const { cwd = process.cwd(), merge, defaults } = options; const results = []; let matchedFiles; const findConfigs = quansync(function* () { if (matchedFiles == null) matchedFiles = []; matchedFiles.length = 0; for (const source of sources) { const { extensions = defaultExtensions } = source; const flatTargets = toArray(source?.files || []).flatMap( (file) => !extensions.length ? [file] : extensions.map((i) => i ? `${file}.${i}` : file) ); const files = yield findUp(flatTargets, { cwd, stopAt: options.stopAt, multiple: merge }); matchedFiles.push([source, files]); } return matchedFiles.flatMap((i) => i[1]); }); const load = quansync(function* (force = false) { if (matchedFiles == null || force) yield findConfigs(); for (const [source, files] of matchedFiles) { if (!files.length) continue; if (!merge) { const result = yield loadConfigFile(files[0], source); if (result) { return { config: applyDefaults(result.config, defaults), sources: result.sources, dependencies: result.dependencies }; } } else { for (const file of files) { const result = yield loadConfigFile(file, source); if (result) { results.push(result); } } } } if (!results.length) { return { config: defaults, sources: [] }; } return { config: applyDefaults(...results.map((i) => i.config), defaults), sources: results.map((i) => i.sources).flat(), dependencies: results.flatMap((i) => i.dependencies || []) }; }); return { load, findConfigs }; } function applyDefaults(...args) { return defu(...args.map((i) => ({ config: i }))).config; } const loadConfig = quansync( function* (options) { return createConfigLoader(options).load(); } ); const loadConfigSync = loadConfig.sync; export { createConfigLoader, defaultExtensions, loadConfig, loadConfigSync };