UNPKG

@deskthing/cli

Version:
397 lines (356 loc) 11.9 kB
import { join, resolve } from "path"; import { createRequire } from "module"; import { DeepPartial, DeskthingConfig } from "./deskthing.config.types"; import { readFile, stat } from "fs/promises"; import { pathToFileURL } from "url"; import { existsSync } from "fs"; export function defineConfig( config: DeepPartial<DeskthingConfig> ): DeepPartial<DeskthingConfig> { return config; } const defaultConfig: DeskthingConfig = { development: { logging: { level: "info", prefix: "[DeskThing Server]", }, client: { logging: { level: "info", prefix: "[DeskThing Client]", enableRemoteLogging: true, }, clientPort: 3000, viteLocation: "http://localhost", vitePort: 5173, linkPort: 8080, }, server: { editCooldownMs: 1000, refreshInterval: 0, }, }, }; async function loadTsConfig( path: string, debug: boolean = false ): Promise<any> { try { const require = createRequire(import.meta.url); try { require("ts-node/register"); } catch (e) { if (debug) console.log(`ts-node not available, continuing anyway...`); // ts-node not available, continue anyway } try { const configModule = require(path); return configModule.default || configModule; } catch (e) { if (debug) console.error( "\x1b[91mError loading TypeScript config:", path, e, "\x1b[0m" ); throw e; } } catch (error) { if (debug) console.error("\x1b[91mError loading config:", error, "\x1b[0m"); throw error; } } const manuallyParseConfig = async ( path: string, debug: boolean = false ): Promise<DeskthingConfig | null> => { try { if (debug) console.log( `(debug mode enabled) Manually loading config from ${path} file and parsing manually (may cause errors)` ); const fileContent = await readFile(path, "utf-8"); // Extract the configuration object from the file content const configMatch = fileContent.match(/defineConfig\s*\(\s*({[\s\S]*?})\s*\)/); if (!configMatch) { if (debug) console.log("Could not find config definition in file using regex"); // Fallback: Try to extract any object between curly braces const objectMatch = fileContent.match(/\{[\s\S]*development[\s\S]*\}/); if (objectMatch) { try { // Convert to valid JSON by replacing single quotes with double quotes // and removing trailing commas let jsonStr = objectMatch[0] .replace(/process\.env\.[A-Z_]+/g, '"ENV_VARIABLE"') .replace(/'/g, '"') .replace(/,(\s*[}\]])/g, "$1") .replace(/\/\/.*$/gm, ""); // Remove comments // Handle environment variables references jsonStr = jsonStr.replace( /process\.env\.[A-Z_]+/g, '"ENV_PLACEHOLDER"' ); const config = JSON.parse(jsonStr); if (debug) console.log("Successfully extracted config using fallback method"); return config; } catch (e) { if (debug) console.log("Failed to parse extracted object:", e); } } throw new Error("Could not find or parse config definition in file"); } try { const configStr = configMatch[1]?.trim() || configMatch[2]?.trim(); if (debug) console.log("Found config string, attempting to evaluate safely"); if (debug) console.log(configStr) // Remove any trailing commas that might break JSON parsing const cleanConfigStr = configStr?.replace(/process\.env\.[A-Z_]+/g, '"ENV_VARIABLE"') .replace(/,(\s*[}\]])/g, "$1") .replace(/\/\/.*$/gm, ""); // Parse as JSON after ensuring it's valid JSON format const config = JSON.parse(cleanConfigStr); return config; } catch (parseError) { if (debug) console.error("\x1b[91mError parsing config:", parseError, "\x1b[0m"); throw parseError; } } catch (e) { if (debug) console.error( "\x1b[91m(debug mode enabled) Error loading config:", e, "\x1b[0m" ); throw e; } }; async function parseTypeScriptConfig(filePath: string, debug: boolean = false): Promise<DeepPartial<DeskthingConfig> | null> { try { const content = await readFile(filePath, 'utf-8'); // Extract the configuration object const configMatch = content.match(/defineConfig\s*\(\s*({[\s\S]*?})\s*\)/); if (!configMatch || !configMatch[1]) { return null; } // Replace TypeScript-specific syntax with JSON-compatible syntax let configStr = configMatch[1] // Handle environment variables .replace(/process\.env\.[A-Z_]+/g, '"ENV_VARIABLE"') // Remove trailing commas .replace(/,(\s*[}\]])/g, '$1') // Remove comments .replace(/\/\/.*$/gm, '') .replace(/\/\*[\s\S]*?\*\//g, ''); // Try to evaluate as JavaScript (safer than eval) try { // Create a function that returns the parsed object const fn = new Function(`return ${configStr}`); return fn(); } catch (evalError) { if (debug) console.log('Failed to evaluate config as JavaScript:', evalError); // Fallback: Try to parse as JSON with some preprocessing try { // Convert single quotes to double quotes for JSON configStr = configStr.replace(/'/g, '"'); return JSON.parse(configStr); } catch (jsonError) { if (debug) console.log('Failed to parse as JSON:', jsonError); return null; } } } catch (error) { if (debug) console.log('Error reading or parsing config file:', error); return null; } } const directConfigImport = async ( path: string, debug: boolean = false ): Promise<DeskthingConfig | null> => { try { if (debug) console.log( `(debug mode enabled) Loading config from ${path} file...` ); const configModule = await import(`${path}`); if (debug) console.log(`Config loaded successfully from ${path}`); return configModule.default || configModule; } catch (error) { if (debug) console.log("Direct import failed:", error); throw error; } }; const directConfigImportUrl = async ( path: string, debug: boolean = false ): Promise<DeskthingConfig | null> => { try { const tsConfigPath = pathToFileURL(path).href; if (debug) console.log( `(debug mode enabled) Loading config from ${tsConfigPath} file...` ); const configModule = await import(tsConfigPath); if (debug) console.log(`Config loaded successfully from ${tsConfigPath}`); return configModule.default || configModule; } catch (error) { if (debug) console.log("Direct import failed:", error); throw error; } }; export const getConfigFromFile = async (debug: boolean = false) => { try { let rootUrl = resolve(process.cwd(), "deskthing.config.ts"); if (!existsSync(rootUrl)) { if (debug) console.log( "deskthing.config.ts not found, trying deskthing.config.js" ); rootUrl = resolve(process.cwd(), "deskthing.config.js"); if (!existsSync(rootUrl)) { throw new Error("No config file found (tried both TS and JS files)"); } } // try direct config import try { const config = await directConfigImport(rootUrl, debug); if (config) { if (debug) console.log("Config loaded successfully from TS file"); return config; } } catch (importError) { // Second attempt: Try to use require with ts-node if direct import fails if (debug) console.log(`Direct import failed, trying alternative method...`); } // try direct config import try { const config = await directConfigImportUrl(rootUrl, debug); if (config) { if (debug) console.log("Config loaded successfully from TS file"); return config; } } catch (importError) { // Second attempt: Try to use require with ts-node if direct import fails if (debug) console.log(`Direct import failed, trying alternative method...`); } // try loading require try { const config = await loadTsConfig(rootUrl, debug); if (config) { if (debug) console.log("Config loaded successfully from TS file"); return config; } } catch (e) { if (debug) console.error("\x1b[91mError loading TS config:", e, "\x1b[0m"); } if (debug) console.log("Trying to parse config manually..."); // try manually parsing config try { const config = await manuallyParseConfig(rootUrl, debug); return config; } catch (e) { if (debug) console.error("\x1b[91mError parsing config manually:", e, "\x1b[0m"); } if (debug) console.log("Trying to parse config and run as js..."); try { const config = await parseTypeScriptConfig(rootUrl, debug); return config; } catch (error) { if (debug) console.log("Error running as js:", error); } } catch (e) { if (debug) console.error( "\x1b[91m(debug mode) Error loading config. Does it exist? :", e, "\x1b[0m" ); return defaultConfig; } }; export let DeskThingConfig: DeskthingConfig = defaultConfig; function isObject(item: any): item is Record<string, any> { return item && typeof item === "object" && !Array.isArray(item); } function deepmerge(target: any, source: any): any { if (!isObject(target) || !isObject(source)) { return source; } const output = { ...target }; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (isObject(source[key]) && isObject(target[key])) { output[key] = deepmerge(target[key], source[key]); } else { output[key] = source[key]; } } } return output; } export const initConfig = async ( options: { silent?: boolean; debug?: boolean } = { silent: false, debug: false, } ) => { try { const userConfig = await getConfigFromFile(options.debug); DeskThingConfig = deepmerge(defaultConfig, userConfig || {}); if (!options.silent || options.debug) { if (userConfig) { console.log(`\n\n\x1b[32m✅ Config Loaded\x1b[0m\n\n`); } else { console.log(`\n\n\x1b[32m✅ No Config Found, Using Default\x1b[0m`); if (options.debug) { console.log( `\x1b[3m\x1b[90mPath Checked: ${join( process.cwd(), "deskthing.config.ts" )}\x1b[0m\n\n` ); } } } } catch (e) { console.warn("\x1b[93mWarning: Error loading config, using defaults\x1b[0m"); if (options.debug) console.error("\x1b[91mError loading config:", e, "\x1b[0m"); DeskThingConfig = defaultConfig; } }; /** * @deprecated - Use {@link DeskThingConfig} instead after calling initConfig in root * @returns */ export async function getServerConfig(): Promise<DeskthingConfig> { if (DeskThingConfig) { return DeskThingConfig; } const userConfig = await getConfigFromFile(); DeskThingConfig = { ...defaultConfig, ...(userConfig || {}), }; return DeskThingConfig; } /** * @deprecated - Use getServerConfig instead */ export const serverConfig: DeskthingConfig = { ...defaultConfig, };