mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
* feat: (AppRunner) Mechanism to configure cli args and derive componentsjs vars from them implemented * fix: (AppRunner) tidying * fix: (AppRunner) tidying up * fix: (AppRunner) runCli method made sync * fix; (VarResolver) refactored to multiple files, and other stylistic fixes. * chore: (AppRunner) Uses builder pattern for yargs base arguments setup to enable better typescript inference * fix(AppRunner): refactoring AppRunner and VarResolver * fix(AppRunner): refactoring AppRunner promise handling * fix(AppRunner): verror dependency removal * fix: Simplify CLI error handling * feat: Use same config for both CLI and app instantiation * fix: Update typings and imports * feat: Split VariableResolver behaviour to 2 classes * feat: Move default value behaviour from CLI to ValueComputers * test: Add unit tests for new CLI classes * feat: Integrate new CLI configuration with all default configurations * feat: Add createApp function to AppRunner * docs: Update comments in CLI-related classes * fix: Various fixes and refactors Co-authored-by: damooo <damodara@protonmail.com>
200 lines
7.9 KiB
TypeScript
200 lines
7.9 KiB
TypeScript
/* eslint-disable unicorn/no-process-exit */
|
|
import type { WriteStream } from 'tty';
|
|
import type { IComponentsManagerBuilderOptions } from 'componentsjs';
|
|
import { ComponentsManager } from 'componentsjs';
|
|
import yargs from 'yargs';
|
|
import { LOG_LEVELS } from '../logging/LogLevel';
|
|
import { getLoggerFor } from '../logging/LogUtil';
|
|
import { createErrorMessage, isError } from '../util/errors/ErrorUtil';
|
|
import { modulePathPlaceholder, resolveAssetPath } from '../util/PathUtil';
|
|
import type { App } from './App';
|
|
import type { CliResolver } from './CliResolver';
|
|
import type { CliArgv, VariableBindings } from './variables/Types';
|
|
|
|
const DEFAULT_CONFIG = `${modulePathPlaceholder}config/default.json`;
|
|
|
|
const DEFAULT_CLI_RESOLVER = 'urn:solid-server-app-setup:default:CliResolver';
|
|
const DEFAULT_APP = 'urn:solid-server:default:App';
|
|
|
|
const CORE_CLI_PARAMETERS = {
|
|
config: { type: 'string', alias: 'c', default: DEFAULT_CONFIG, requiresArg: true },
|
|
loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS },
|
|
mainModulePath: { type: 'string', alias: 'm', requiresArg: true },
|
|
} as const;
|
|
|
|
/**
|
|
* A class that can be used to instantiate and start a server based on a Component.js configuration.
|
|
*/
|
|
export class AppRunner {
|
|
private readonly logger = getLoggerFor(this);
|
|
|
|
/**
|
|
* Starts the server with a given config.
|
|
* This method can be used to start the server from within another JavaScript application.
|
|
* Keys of the `variableBindings` object should be Components.js variables.
|
|
* E.g.: `{ 'urn:solid-server:default:variable:rootFilePath': '.data' }`.
|
|
*
|
|
* @param loaderProperties - Components.js loader properties.
|
|
* @param configFile - Path to the server config file.
|
|
* @param variableBindings - Parameters to pass into the VariableResolver.
|
|
*/
|
|
public async run(
|
|
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
|
configFile: string,
|
|
variableBindings: VariableBindings,
|
|
): Promise<void> {
|
|
const app = await this.create(loaderProperties, configFile, variableBindings);
|
|
await app.start();
|
|
}
|
|
|
|
/**
|
|
* Returns an App object, created with the given config, that can start and stop the Solid server.
|
|
* Keys of the `variableBindings` object should be Components.js variables.
|
|
* E.g.: `{ 'urn:solid-server:default:variable:rootFilePath': '.data' }`.
|
|
*
|
|
* @param loaderProperties - Components.js loader properties.
|
|
* @param configFile - Path to the server config file.
|
|
* @param variableBindings - Bindings of Components.js variables.
|
|
*/
|
|
public async create(
|
|
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
|
configFile: string,
|
|
variableBindings: VariableBindings,
|
|
): Promise<App> {
|
|
// Create a resolver to translate (non-core) CLI parameters into values for variables
|
|
const componentsManager = await this.createComponentsManager<App>(loaderProperties, configFile);
|
|
|
|
// Create the application using the translated variable values
|
|
return componentsManager.instantiate(DEFAULT_APP, { variables: variableBindings });
|
|
}
|
|
|
|
/**
|
|
* Starts the server as a command-line application.
|
|
* Will exit the process on failure.
|
|
*
|
|
* Made non-async to lower the risk of unhandled promise rejections.
|
|
* This is only relevant when this is used to start as a Node.js application on its own,
|
|
* if you use this as part of your code you probably want to use the async version.
|
|
*
|
|
* @param argv - Command line arguments.
|
|
* @param stderr - Stream that should be used to output errors before the logger is enabled.
|
|
*/
|
|
public runCliSync({ argv, stderr = process.stderr }: { argv?: CliArgv; stderr?: WriteStream }): void {
|
|
this.runCli(argv).catch((error): never => {
|
|
stderr.write(createErrorMessage(error));
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Starts the server as a command-line application.
|
|
* @param argv - Command line arguments.
|
|
*/
|
|
public async runCli(argv?: CliArgv): Promise<void> {
|
|
const app = await this.createCli(argv);
|
|
try {
|
|
await app.start();
|
|
} catch (error: unknown) {
|
|
this.logger.error(`Could not start the server: ${createErrorMessage(error)}`);
|
|
this.resolveError('Could not start the server', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an App object, created by parsing the Command line arguments, that can start and stop the Solid server.
|
|
* Will exit the process on failure.
|
|
*
|
|
* @param argv - Command line arguments.
|
|
*/
|
|
public async createCli(argv: CliArgv = process.argv): Promise<App> {
|
|
// Parse only the core CLI arguments needed to load the configuration
|
|
const yargv = yargs(argv.slice(2))
|
|
.usage('node ./bin/server.js [args]')
|
|
.options(CORE_CLI_PARAMETERS)
|
|
// We disable help here as it would only show the core parameters
|
|
.help(false);
|
|
|
|
const params = await yargv.parse();
|
|
|
|
const loaderProperties = {
|
|
mainModulePath: resolveAssetPath(params.mainModulePath),
|
|
dumpErrorState: true,
|
|
logLevel: params.loggingLevel,
|
|
};
|
|
|
|
const config = resolveAssetPath(params.config);
|
|
|
|
// Create the Components.js manager used to build components from the provided config
|
|
let componentsManager: ComponentsManager<any>;
|
|
try {
|
|
componentsManager = await this.createComponentsManager(loaderProperties, config);
|
|
} catch (error: unknown) {
|
|
// Print help of the expected core CLI parameters
|
|
const help = await yargv.getHelp();
|
|
this.resolveError(`${help}\n\nCould not build the config files from ${config}`, error);
|
|
}
|
|
|
|
// Build the CLI components and use them to generate values for the Components.js variables
|
|
const variables = await this.resolveVariables(componentsManager, argv);
|
|
|
|
// Build and start the actual server application using the generated variable values
|
|
return await this.createApp(componentsManager, variables);
|
|
}
|
|
|
|
/**
|
|
* Creates the Components Manager that will be used for instantiating.
|
|
*/
|
|
public async createComponentsManager<T>(
|
|
loaderProperties: IComponentsManagerBuilderOptions<T>,
|
|
configFile: string,
|
|
): Promise<ComponentsManager<T>> {
|
|
const componentsManager = await ComponentsManager.build(loaderProperties);
|
|
await componentsManager.configRegistry.register(configFile);
|
|
return componentsManager;
|
|
}
|
|
|
|
/**
|
|
* Handles the first Components.js instantiation,
|
|
* where CLI settings and variable mappings are created.
|
|
*/
|
|
private async resolveVariables(componentsManager: ComponentsManager<CliResolver>, argv: string[]):
|
|
Promise<VariableBindings> {
|
|
try {
|
|
// Create a CliResolver, which combines a CliExtractor and a VariableResolver
|
|
const resolver = await componentsManager.instantiate(DEFAULT_CLI_RESOLVER, {});
|
|
// Convert CLI args to CLI bindings
|
|
const cliValues = await resolver.cliExtractor.handleSafe(argv);
|
|
// Convert CLI bindings into variable bindings
|
|
return await resolver.settingsResolver.handleSafe(cliValues);
|
|
} catch (error: unknown) {
|
|
this.resolveError(`Could not load the config variables`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The second Components.js instantiation,
|
|
* where the App is created and started using the variable mappings.
|
|
*/
|
|
private async createApp(componentsManager: ComponentsManager<App>, variables: Record<string, unknown>): Promise<App> {
|
|
try {
|
|
// Create the app
|
|
return await componentsManager.instantiate(DEFAULT_APP, { variables });
|
|
} catch (error: unknown) {
|
|
this.resolveError(`Could not create the server`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws a new error that provides additional information through the extra message.
|
|
* Also appends the stack trace to the message.
|
|
* This is needed for errors that are thrown before the logger is created as we can't log those the standard way.
|
|
*/
|
|
private resolveError(message: string, error: unknown): never {
|
|
let errorMessage = `${message}\nCause: ${createErrorMessage(error)}\n`;
|
|
if (isError(error)) {
|
|
errorMessage += `${error.stack}\n`;
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
}
|