diff --git a/server.js b/server.js index 76a9c6585..e43c0baf0 100644 --- a/server.js +++ b/server.js @@ -20,10 +20,28 @@ import open from 'open'; // local library imports import './src/fetch-patch.js'; -import { serverEvents, EVENT_NAMES } from './src/server-events.js'; import { CommandLineParser } from './src/command-line.js'; -import { loadPlugins } from './src/plugin-loader.js'; -import { +import { serverDirectory } from './src/server-directory.js'; + +console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); + +// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. +// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 +// Safe to remove once support for Node v20 is dropped. +if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { + // @ts-ignore + if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); +} + +// config.yaml will be set when parsing command line arguments +const cliArgs = new CommandLineParser().parse(process.argv); +globalThis.DATA_ROOT = cliArgs.dataRoot; +globalThis.COMMAND_LINE_ARGS = cliArgs; +process.chdir(serverDirectory); + +const { serverEvents, EVENT_NAMES } = await import('./src/server-events.js'); +const { loadPlugins } = await import('./src/plugin-loader.js'); +const { initUserStorage, getCookieSecret, getCookieSessionName, @@ -38,17 +56,17 @@ import { getSessionCookieAge, verifySecuritySettings, loginPageMiddleware, -} from './src/users.js'; +} = await import('./src/users.js'); -import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; -import basicAuthMiddleware from './src/middleware/basicAuth.js'; -import getWhitelistMiddleware from './src/middleware/whitelist.js'; -import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js'; -import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; -import initRequestProxy from './src/request-proxy.js'; -import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; -import corsProxyMiddleware from './src/middleware/corsProxy.js'; -import { +const { default: getWebpackServeMiddleware } = await import('./src/middleware/webpack-serve.js'); +const { default: basicAuthMiddleware } = await import('./src/middleware/basicAuth.js'); +const { default: getWhitelistMiddleware } = await import('./src/middleware/whitelist.js'); +const { default: accessLoggerMiddleware, getAccessLogPath, migrateAccessLog } = await import('./src/middleware/accessLogWriter.js'); +const { default: multerMonkeyPatch } = await import('./src/middleware/multerMonkeyPatch.js'); +const { default: initRequestProxy } = await import('./src/request-proxy.js'); +const { default: getCacheBusterMiddleware } = await import('./src/middleware/cacheBuster.js'); +const { default: corsProxyMiddleware } = await import('./src/middleware/corsProxy.js'); +const { getVersion, color, removeColorFormatting, @@ -56,38 +74,23 @@ import { safeReadFileSync, setupLogLevel, setWindowTitle, -} from './src/util.js'; -import { UPLOADS_DIRECTORY } from './src/constants.js'; -import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; -import { serverDirectory } from './src/server-directory.js'; +} = await import('./src/util.js'); +const { UPLOADS_DIRECTORY } = await import('./src/constants.js'); +const { ensureThumbnailCache } = await import('./src/endpoints/thumbnails.js'); // Routers -import { router as usersPublicRouter } from './src/endpoints/users-public.js'; -import { init as statsInit, onExit as statsOnExit } from './src/endpoints/stats.js'; -import { checkForNewContent } from './src/endpoints/content-manager.js'; -import { init as settingsInit } from './src/endpoints/settings.js'; -import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './src/server-startup.js'; -import { diskCache } from './src/endpoints/characters.js'; +const { router : usersPublicRouter } = await import('./src/endpoints/users-public.js'); +const { init : statsInit, onExit : statsOnExit } = await import('./src/endpoints/stats.js'); +const { checkForNewContent } = await import('./src/endpoints/content-manager.js'); +const { init : settingsInit } = await import('./src/endpoints/settings.js'); +const { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } = await import('./src/server-startup.js'); +const { diskCache } = await import('./src/endpoints/characters.js'); // Unrestrict console logs display limit util.inspect.defaultOptions.maxArrayLength = null; util.inspect.defaultOptions.maxStringLength = null; util.inspect.defaultOptions.depth = 4; -console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); - -// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. -// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 -// Safe to remove once support for Node v20 is dropped. -if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { - // @ts-ignore - if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); -} - -const cliArgs = new CommandLineParser().parse(process.argv); -globalThis.DATA_ROOT = cliArgs.dataRoot; -globalThis.COMMAND_LINE_ARGS = cliArgs; - if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) { console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); process.exit(1); diff --git a/src/command-line.js b/src/command-line.js index 002f46926..c7d4d9d9b 100644 --- a/src/command-line.js +++ b/src/command-line.js @@ -1,10 +1,11 @@ import yargs from 'yargs/yargs'; import { hideBin } from 'yargs/helpers'; import ipRegex from 'ip-regex'; -import { canResolve, color, getConfigValue, stringToBool } from './util.js'; +import { canResolve, color, getConfigValue, setConfigFilePath, stringToBool } from './util.js'; /** * @typedef {object} CommandLineArguments Parsed command line arguments + * @property {string} configPath Path to the config file * @property {string} dataRoot Data root directory * @property {number} port Port number * @property {boolean} listen If SillyTavern is listening on all network interfaces @@ -40,6 +41,7 @@ export class CommandLineParser { constructor() { /** @type {CommandLineArguments} */ this.default = Object.freeze({ + configPath: './config.yaml', dataRoot: './data', port: 8000, listen: false, @@ -88,6 +90,11 @@ export class CommandLineParser { parse(args) { const cliArguments = yargs(hideBin(args)) .usage('Usage: [options]\nOptions that are not provided will be filled with config values.') + .option('configPath', { + type: 'string', + default: null, + describe: 'Path to the config file', + }) .option('enableIPv6', { type: 'string', default: null, @@ -177,8 +184,11 @@ export class CommandLineParser { describe: 'Request proxy bypass list (space separated list of hosts)', }).parseSync(); + const configPath = cliArguments.configPath ?? this.default.configPath; + setConfigFilePath(configPath); /** @type {CommandLineArguments} */ const result = { + configPath: configPath, dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot), port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'), listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'), diff --git a/src/util.js b/src/util.js index 45cc3f1f7..c12091910 100644 --- a/src/util.js +++ b/src/util.js @@ -23,6 +23,7 @@ import { serverDirectory } from './server-directory.js'; * Parsed config object. */ let CACHED_CONFIG = null; +let CONFIG_FILE = null; /** * Converts a configuration key to an environment variable key. @@ -32,23 +33,38 @@ let CACHED_CONFIG = null; */ export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_'); +/** + * Set the config file path. + * @param {string} configFilePath Path to the config file + */ +export function setConfigFilePath(configFilePath) { + if (CONFIG_FILE !== null) { + console.error(color.red('Config file path already set. Please restart the server to change the config file path.')); + } + CONFIG_FILE = path.resolve(configFilePath); +} + /** * Returns the config object from the config.yaml file. * @returns {object} Config object */ export function getConfig() { + if (CONFIG_FILE === null) { + console.trace(); + console.error(color.red('No config file path set. Please set the config file path using setConfigFilePath().')); + process.exit(1); + } if (CACHED_CONFIG) { return CACHED_CONFIG; } - - if (!fs.existsSync('./config.yaml')) { + if (!fs.existsSync(CONFIG_FILE)) { console.error(color.red('No config file found. Please create a config.yaml file. The default config file can be found in the /default folder.')); console.error(color.red('The program will now exit.')); process.exit(1); } try { - const config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8')); + const config = yaml.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); CACHED_CONFIG = config; return config; } catch (error) {