@deskthing/cli
Version:
An emulator for the DeskThing Server
397 lines (356 loc) • 11.9 kB
text/typescript
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,
};