UNPKG

@raven-js/fledge

Version:

From nestling to flight-ready - Build & bundle tool for modern JavaScript apps

210 lines (181 loc) 6.03 kB
/** * @author Anonyfox <max@anonyfox.com> * @license MIT * @see {@link https://ravenjs.dev} * @see {@link https://github.com/Anonyfox/ravenjs} * @see {@link https://anonyfox.com} */ /** * @file Configuration import utilities for binary mode. * * Pure functions for loading and validating binary configuration from various * sources: files, strings, and objects. Handles ESM imports and validation. */ import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; /** * Import configuration from JavaScript string (piped input) * @param {string} configString - JavaScript configuration code * @param {string} [exportName] - Optional named export to select (uses default if not specified) * @returns {Promise<Object>} Configuration object * @throws {Error} If string is empty, invalid JavaScript, or doesn't export object */ export async function importConfigFromString(configString, exportName) { if (!configString || typeof configString !== "string") { throw new Error("Configuration string cannot be empty"); } const trimmed = configString.trim(); if (!trimmed) { throw new Error("Configuration string cannot be whitespace only"); } try { // Create data URL from config string for dynamic import const base64Config = btoa(trimmed); const dataUrl = `data:text/javascript;base64,${base64Config}`; const module = await import(dataUrl); let config; // Use named export if specified if (exportName) { if (!(exportName in module)) { throw new Error( `Export '${exportName}' not found in configuration string`, ); } config = module[exportName]; } else { // Use default export or throw error if (!("default" in module)) { throw new Error("Configuration must export a default object"); } config = module.default; } if (!config || typeof config !== "object") { throw new Error("Configuration must export an object"); } return config; } catch (error) { const err = /** @type {Error} */ (error); if ( err.message.includes("Configuration must export") || err.message.includes("Export ") || err.message.includes("not found") ) { throw error; // Re-throw our validation errors } throw new Error(`Failed to parse configuration string: ${err.message}`); } } /** * Import configuration from JavaScript file * @param {string} configPath - Path to configuration file * @param {string} [exportName] - Optional named export to select (uses default if not specified) * @returns {Promise<Object>} Configuration object * @throws {Error} If file doesn't exist, invalid JavaScript, or doesn't export object */ export async function importConfigFromFile(configPath, exportName) { if (!configPath || typeof configPath !== "string") { throw new Error("Configuration file path cannot be empty"); } const trimmed = configPath.trim(); if (!trimmed) { throw new Error("Configuration file path cannot be whitespace only"); } try { // Convert to file URL for dynamic import const fileUrl = pathToFileURL(resolve(trimmed)).href; const module = await import(fileUrl); let config; // Use named export if specified if (exportName) { if (!(exportName in module)) { throw new Error(`Export '${exportName}' not found in ${configPath}`); } config = module[exportName]; } else { // Use default export or throw error if (!("default" in module)) { throw new Error("Configuration must export a default object"); } config = module.default; } if (!config || typeof config !== "object") { throw new Error("Configuration must export an object"); } return config; } catch (error) { const err = /** @type {Error} */ (error); if ( err.message.includes("ENOENT") || err.message.includes("Cannot find module") ) { throw new Error(`Configuration file not found: ${configPath}`); } if ( err.message.includes("Configuration must export") || err.message.includes("Export ") || err.message.includes("not found") ) { throw error; // Re-throw our validation errors } throw new Error(`Failed to import configuration file: ${err.message}`); } } /** * Validate configuration object structure for binary mode * @param {unknown} config - Configuration object to validate * @throws {Error} If configuration is invalid */ export function validateConfigObject(config) { if (!config || typeof config !== "object" || Array.isArray(config)) { throw new Error("Configuration must be an object"); } const cfg = /** @type {any} */ (config); // Required fields if (typeof cfg.entry !== "string") { throw new Error("Configuration must specify 'entry' as a string"); } // Optional field validation if (cfg.output !== undefined && typeof cfg.output !== "string") { throw new Error("Configuration 'output' must be a string"); } if ( cfg.bundles && (typeof cfg.bundles !== "object" || Array.isArray(cfg.bundles)) ) { throw new Error("Configuration 'bundles' must be an object"); } if (cfg.bundles) { for (const [, value] of Object.entries(cfg.bundles)) { if (typeof value !== "string") { throw new Error("Configuration 'bundles' must map strings to strings"); } } } if (cfg.sea && (typeof cfg.sea !== "object" || Array.isArray(cfg.sea))) { throw new Error("Configuration 'sea' must be an object"); } if ( cfg.signing && (typeof cfg.signing !== "object" || Array.isArray(cfg.signing)) ) { throw new Error("Configuration 'signing' must be an object"); } if (cfg.signing?.enabled && typeof cfg.signing.enabled !== "boolean") { throw new Error("Configuration 'signing.enabled' must be a boolean"); } if ( cfg.signing?.identity && typeof cfg.signing.identity !== "string" && cfg.signing.identity !== undefined ) { throw new Error( "Configuration 'signing.identity' must be a string or undefined", ); } if ( cfg.metadata && (typeof cfg.metadata !== "object" || Array.isArray(cfg.metadata)) ) { throw new Error("Configuration 'metadata' must be an object"); } }