From 3137ed0a95869741cf79812e1d9806e6a9560ffa Mon Sep 17 00:00:00 2001 From: damooo Date: Wed, 17 Nov 2021 11:20:40 +0530 Subject: [PATCH 01/18] feat: (AppRunner) Mechanism to configure cli args and derive componentsjs vars from them implemented --- config/app-setup/vars.json | 110 ++++++++++++++++++++++++ src/index.ts | 1 + src/init/AppRunner.ts | 115 +++++++++++-------------- src/init/VarResolver.ts | 139 +++++++++++++++++++++++++++++++ test/unit/init/AppRunner.test.ts | 85 ++++--------------- 5 files changed, 315 insertions(+), 135 deletions(-) create mode 100644 config/app-setup/vars.json create mode 100644 src/init/VarResolver.ts diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json new file mode 100644 index 0000000000..b6376136c7 --- /dev/null +++ b/config/app-setup/vars.json @@ -0,0 +1,110 @@ +{ + "@context": [ + "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld" + ], + "@graph": [ + { + "comment": "VarResolver resolves variables to be used in app instantiation, from cli args", + "@id": "urn:solid-server-app-setup:default:VarResolver", + "@type": "VarResolver", + "opts_usage": "node ./bin/server.js [args]", + "opts_strictMode": true, + "opts_yargsOptions": [ + { + "@type": "IYargsOptions", + "VarResolver:_opts_yargsOptions_key": "baseUrl", + "VarResolver:_opts_alias": "b", + "VarResolver:_opts_requiresArg": true, + "VarResolver:_opts_type": "string" + }, + { + "@type": "IYargsOptions", + "VarResolver:_opts_yargsOptions_key": "port", + "VarResolver:_opts_alias": "p", + "VarResolver:_opts_requiresArg": true, + "VarResolver:_opts_type": "number", + "VarResolver:_opts_default": 3000 + }, + { + "@type": "IYargsOptions", + "VarResolver:_opts_yargsOptions_key": "rootFilePath", + "VarResolver:_opts_alias": "f", + "VarResolver:_opts_requiresArg": true, + "VarResolver:_opts_type": "string", + "VarResolver:_opts_default": "./" + }, + { + "@type": "IYargsOptions", + "VarResolver:_opts_yargsOptions_key": "showStackTrace", + "VarResolver:_opts_alias": "t", + "VarResolver:_opts_type": "boolean", + "VarResolver:_opts_default": false + }, + { + "@type": "IYargsOptions", + "VarResolver:_opts_yargsOptions_key": "sparqlEndpoint", + "VarResolver:_opts_alias": "s", + "VarResolver:_opts_requiresArg": true, + "VarResolver:_opts_type": "string" + }, + { + "@type": "IYargsOptions", + "VarResolver:_opts_yargsOptions_key": "podConfigJson", + "VarResolver:_opts_requiresArg": true, + "VarResolver:_opts_type": "string", + "VarResolver:_opts_default": "./pod-config.json" + } + ], + "opts_varComputers": [ + { + "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:baseUrl", + "VarResolver:_opts_varComputers_value": { + "@type": "BaseUrlComputer" + } + }, + { + "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:loggingLevel", + "VarResolver:_opts_varComputers_value": { + "@type": "ArgExtractor", + "argKey": "loggingLevel" + } + }, + { + "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:port", + "VarResolver:_opts_varComputers_value": { + "@type": "ArgExtractor", + "argKey": "port" + } + }, + { + "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:rootFilePath", + "VarResolver:_opts_varComputers_value": { + "@type": "AssetPathResolver", + "pathArgKey": "rootFilePath" + } + }, + { + "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:sparqlEndpoint", + "VarResolver:_opts_varComputers_value": { + "@type": "ArgExtractor", + "argKey": "sparqlEndpoint" + } + }, + { + "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:showStackTrace", + "VarResolver:_opts_varComputers_value": { + "@type": "ArgExtractor", + "argKey": "showStackTrace" + } + }, + { + "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:AssetPathResolver", + "VarResolver:_opts_varComputers_value": { + "@type": "AssetPathResolver", + "pathArgKey": "podConfigJson" + } + } + ] + } + ] +} diff --git a/src/index.ts b/src/index.ts index 8621ee257e..4dc4fce9dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -185,6 +185,7 @@ export * from './init/ContainerInitializer'; export * from './init/Initializer'; export * from './init/LoggerInitializer'; export * from './init/ServerInitializer'; +export * from './init/VarResolver'; // Logging export * from './logging/LazyLogger'; diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 87bda754ef..158fa158e3 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -5,10 +5,13 @@ import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; import yargs from 'yargs'; import { getLoggerFor } from '../logging/LogUtil'; -import { ensureTrailingSlash, resolveAssetPath, modulePathPlaceholder } from '../util/PathUtil'; +import { ensureTrailingSlash, resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; +import type { VarResolver } from './VarResolver'; +import { baseYargsOptions } from './VarResolver'; -const defaultConfig = `${modulePathPlaceholder}config/default.json`; +const varConfigComponentIri = 'urn:solid-server-app-setup:default:VarResolver'; +const appComponentIri = 'urn:solid-server:default:App'; export interface CliParams { loggingLevel: string; @@ -35,7 +38,7 @@ export class AppRunner { configFile: string, variableParams: CliParams, ): Promise { - const app = await this.createApp(loaderProperties, configFile, variableParams); + const app = await this.createComponent(loaderProperties, configFile, variableParams, appComponentIri); await app.start(); } @@ -45,7 +48,7 @@ export class AppRunner { * @param args - Command line arguments. * @param stderr - Standard error stream. */ - public runCli({ + public async runCli({ argv = process.argv, stderr = process.stderr, }: { @@ -53,85 +56,63 @@ export class AppRunner { stdin?: ReadStream; stdout?: WriteStream; stderr?: WriteStream; - } = {}): void { + } = {}): Promise { // Parse the command-line arguments - // eslint-disable-next-line no-sync - const params = yargs(argv.slice(2)) - .strict() + const params = await yargs(argv.slice(2)) .usage('node ./bin/server.js [args]') - .check((args): boolean => { - if (args._.length > 0) { - throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); - } - for (const key of Object.keys(args)) { - // We have no options that allow for arrays - const val = args[key]; - if (key !== '_' && Array.isArray(val)) { - throw new Error(`Multiple values were provided for: "${key}": "${val.join('", "')}"`); - } - } - return true; - }) - .options({ - baseUrl: { type: 'string', alias: 'b', requiresArg: true }, - config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, - loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true }, - mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, - port: { type: 'number', alias: 'p', default: 3000, requiresArg: true }, - rootFilePath: { type: 'string', alias: 'f', default: './', requiresArg: true }, - showStackTrace: { type: 'boolean', alias: 't', default: false }, - sparqlEndpoint: { type: 'string', alias: 's', requiresArg: true }, - podConfigJson: { type: 'string', default: './pod-config.json', requiresArg: true }, - }) - .parseSync(); + .options(baseYargsOptions) + .parse(); // Gather settings for instantiating the server - const loaderProperties: IComponentsManagerBuilderOptions = { - mainModulePath: resolveAssetPath(params.mainModulePath), + const loaderProperties = { + mainModulePath: resolveAssetPath(params.mainModulePath as string), dumpErrorState: true, logLevel: params.loggingLevel as LogLevel, }; - const configFile = resolveAssetPath(params.config); - // Create and execute the app - this.createApp(loaderProperties, configFile, params) - .then( - async(app): Promise => app.start(), - (error: Error): void => { - // Instantiation of components has failed, so there is no logger to use - stderr.write(`Error: could not instantiate server from ${configFile}\n`); - stderr.write(`${error.stack}\n`); - process.exit(1); - }, - ).catch((error): void => { - this.logger.error(`Could not start server: ${error}`, { error }); - process.exit(1); - }); - } + // Create varResolver + const varConfigFile = resolveAssetPath(params.varConfig as string); + const varResolver = await this.createComponent( + loaderProperties as IComponentsManagerBuilderOptions, varConfigFile, {}, varConfigComponentIri, + ); + // Resolve vars for app startup + const vars = await varResolver.resolveVars(argv); - /** - * Creates the main app object to start the server from a given config. - * @param loaderProperties - Components.js loader properties. - * @param configFile - Path to a Components.js config file. - * @param variables - Variables to pass into the config file. - */ - public async createApp( - loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, - variables: CliParams | Record, - ): Promise { - // Translate command-line parameters if needed - if (typeof variables.loggingLevel === 'string') { - variables = this.createVariables(variables as CliParams); + const configFile = resolveAssetPath(params.config as string); + let app: App; + + // Create app + try { + app = await this.createComponent( + loaderProperties as IComponentsManagerBuilderOptions, configFile, vars, appComponentIri, + ); + } catch (error: unknown) { + stderr.write(`Error: could not instantiate server from ${configFile}\n`); + stderr.write(`${(error as Error).stack}\n`); + process.exit(1); } + // Execute app + try { + await app.start(); + } catch (error: unknown) { + this.logger.error(`Could not start server: ${error}`, { error }); + process.exit(1); + } + } + + public async createComponent( + loaderProperties: IComponentsManagerBuilderOptions, + configFile: string, + variables: Record, + componentIri: string, + ): Promise { // Set up Components.js const componentsManager = await ComponentsManager.build(loaderProperties); await componentsManager.configRegistry.register(configFile); // Create the app - const app = 'urn:solid-server:default:App'; - return await componentsManager.instantiate(app, { variables }); + return await componentsManager.instantiate(componentIri, { variables }); } /** diff --git a/src/init/VarResolver.ts b/src/init/VarResolver.ts new file mode 100644 index 0000000000..b98d42dab7 --- /dev/null +++ b/src/init/VarResolver.ts @@ -0,0 +1,139 @@ +import yargs from 'yargs'; +import { AsyncHandler } from '../util/handlers/AsyncHandler'; +import { ensureTrailingSlash, modulePathPlaceholder, resolveAssetPath } from '../util/PathUtil'; + +const defaultConfig = `${modulePathPlaceholder}config/default.json`; +const defaultVarConfig = `${modulePathPlaceholder}config/app-setup/vars.json`; + +/** + * CLI options needed for performing meta-process of app initialization. + * These options doesn't contribute to components-js vars normally. + */ +export const baseYargsOptions: Record = { + config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, + loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true }, + mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, + varConfig: { type: 'string', alias: 'v', default: defaultVarConfig, requiresArg: true }, +}; + +/** + * A handler that takes args, and returns computed variable value + */ +export abstract class VarComputer extends AsyncHandler { + public abstract handle(args: yargs.Arguments): Promise; +} + +// Currently componentsjs-builder giving error, if we use external TSTypeReference. +// Re-definition is temporary work around. +export interface IYargsOptions extends yargs.Options { + alias?: string | readonly string[]; + default?: any; + desc?: string; + requiresArg?: boolean; + type?: 'array' | 'count' | yargs.PositionalOptionsType; +} + +export interface IVarResolverOptions { + yargsOptions: Record; + varComputers: Record; + usage?: string; + strictMode?: boolean; +} + +export class VarResolver { + protected readonly opts: IVarResolverOptions; + protected readonly effectiveYargsOptions: Record; + + public constructor(opts: IVarResolverOptions) { + this.opts = opts; + this.effectiveYargsOptions = { + ...opts.yargsOptions, + ...baseYargsOptions, + }; + } + + private async parseArgs(argv: readonly string[]): Promise { + let yArgv = yargs(argv.slice(2)); + if (this.opts.usage !== undefined) { + yArgv = yArgv.usage(this.opts.usage); + } + if (this.opts.strictMode) { + yArgv = yArgv.strict(); + } + + yArgv = yArgv.check((args): boolean => { + if (args._.length > 0) { + throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); + } + for (const key of Object.keys(args)) { + const val = args[key]; + if (key !== '_' && Array.isArray(val)) { + throw new Error(`Multiple values were provided for: "${key}": "${val.join('", "')}"`); + } + } + return true; + }).options(this.effectiveYargsOptions); + + return yArgv.parse(); + } + + private async computeVars(args: yargs.Arguments): Promise> { + const vars: Record = {}; + for (const varId of Object.keys(this.opts.varComputers)) { + const varComputer = this.opts.varComputers[varId]; + try { + vars[varId] = await varComputer.handle(args); + } catch (err: unknown) { + throw new Error(`Error in computing value for variable ${varId}: ${err}`); + } + } + return vars; + } + + public async resolveVars(argv: readonly string[]): Promise> { + const args = await this.parseArgs(argv); + return this.computeVars(args); + } +} + +/** + * Simple VarComputer that just extracts an arg from parsed args. + */ +export class ArgExtractor extends VarComputer { + private readonly argKey: string; + + public constructor(argKey: string) { + super(); + this.argKey = argKey; + } + + public async handle(args: yargs.Arguments): Promise { + return args[this.argKey]; + } +} + +export class AssetPathResolver extends VarComputer { + private readonly pathArgKey: string; + + public constructor(pathArgKey: string) { + super(); + this.pathArgKey = pathArgKey; + } + + public async handle(args: yargs.Arguments): Promise { + const path = args[this.pathArgKey]; + if (typeof path !== 'string') { + throw new Error(`Invalid ${this.pathArgKey} argument`); + } + return resolveAssetPath(path); + } +} + +/** + * A handler to compute base-url from args + */ +export class BaseUrlComputer extends VarComputer { + public async handle(args: yargs.Arguments): Promise { + return args.baseUrl ? ensureTrailingSlash(args.baseUrl as string) : `http://localhost:${args.port}/`; + } +} diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index ec206e4df7..66384a01e8 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -80,16 +80,11 @@ describe('AppRunner', (): void => { describe('runCli', (): void => { it('starts the server with default settings.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script' ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - - expect(ComponentsManager.build).toHaveBeenCalledTimes(1); + expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'info', @@ -118,7 +113,7 @@ describe('AppRunner', (): void => { }); it('accepts abbreviated flags.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script', '-b', 'http://pod.example/', @@ -133,12 +128,7 @@ describe('AppRunner', (): void => { ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - - expect(ComponentsManager.build).toHaveBeenCalledTimes(1); + expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'debug', @@ -163,7 +153,7 @@ describe('AppRunner', (): void => { }); it('accepts full flags.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script', '--baseUrl', 'http://pod.example/', @@ -178,12 +168,7 @@ describe('AppRunner', (): void => { ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - - expect(ComponentsManager.build).toHaveBeenCalledTimes(1); + expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'debug', @@ -208,13 +193,12 @@ describe('AppRunner', (): void => { }); it('accepts asset paths for the config flag.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script', '--config', '@css:config/file.json', ], }); - await new Promise(setImmediate); expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register).toHaveBeenCalledWith( @@ -237,14 +221,9 @@ describe('AppRunner', (): void => { '--podConfigJson', '/different-path.json', ]; - new AppRunner().runCli(); + await new AppRunner().runCli(); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - - expect(ComponentsManager.build).toHaveBeenCalledTimes(1); + expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'debug', @@ -272,19 +251,14 @@ describe('AppRunner', (): void => { it('exits with output to stderr when instantiation fails.', async(): Promise => { manager.instantiate.mockRejectedValueOnce(new Error('Fatal')); - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script' ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - - expect(write).toHaveBeenCalledTimes(2); + expect(write).toHaveBeenCalledTimes((1)); expect(write).toHaveBeenNthCalledWith(1, expect.stringMatching(/^Error: could not instantiate server from .*default\.json/u)); - expect(write).toHaveBeenNthCalledWith(2, + expect(write).toHaveBeenNthCalledWith((1), expect.stringMatching(/^Error: Fatal/u)); expect(exit).toHaveBeenCalledTimes(1); @@ -293,75 +267,50 @@ describe('AppRunner', (): void => { it('exits without output to stderr when initialization fails.', async(): Promise => { app.start.mockRejectedValueOnce(new Error('Fatal')); - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script' ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - expect(write).toHaveBeenCalledTimes(0); expect(exit).toHaveBeenCalledWith(1); }); it('exits when unknown options are passed to the main executable.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script', '--foo' ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - expect(error).toHaveBeenCalledWith('Unknown argument: foo'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); it('exits when no value is passed to the main executable for an argument.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script', '-s' ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - expect(error).toHaveBeenCalledWith('Not enough arguments following: s'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); it('exits when unknown parameters are passed to the main executable.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script', 'foo', 'bar', 'foo.txt', 'bar.txt' ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - expect(error).toHaveBeenCalledWith('Unsupported positional arguments: "foo", "bar", "foo.txt", "bar.txt"'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); it('exits when multiple values for a parameter are passed.', async(): Promise => { - new AppRunner().runCli({ + await new AppRunner().runCli({ argv: [ 'node', 'script', '-l', 'info', '-l', 'debug' ], }); - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - expect(error).toHaveBeenCalledWith('Multiple values were provided for: "l": "info", "debug"'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); From f723690a672dcf2691e8cb14ab6837bd700335b6 Mon Sep 17 00:00:00 2001 From: damooo Date: Wed, 17 Nov 2021 12:15:28 +0530 Subject: [PATCH 02/18] fix: (AppRunner) tidying --- src/init/AppRunner.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 158fa158e3..c326252f26 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -114,20 +114,4 @@ export class AppRunner { // Create the app return await componentsManager.instantiate(componentIri, { variables }); } - - /** - * Translates command-line parameters into Components.js variables. - */ - protected createVariables(params: CliParams): Record { - return { - 'urn:solid-server:default:variable:baseUrl': - params.baseUrl ? ensureTrailingSlash(params.baseUrl) : `http://localhost:${params.port}/`, - 'urn:solid-server:default:variable:loggingLevel': params.loggingLevel, - 'urn:solid-server:default:variable:port': params.port, - 'urn:solid-server:default:variable:rootFilePath': resolveAssetPath(params.rootFilePath), - 'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint, - 'urn:solid-server:default:variable:showStackTrace': params.showStackTrace, - 'urn:solid-server:default:variable:podConfigJson': resolveAssetPath(params.podConfigJson), - }; - } } From f935876bde23cc4c910171151a24ff92646b0ef0 Mon Sep 17 00:00:00 2001 From: damooo Date: Wed, 17 Nov 2021 13:25:46 +0530 Subject: [PATCH 03/18] fix: (AppRunner) tidying up --- src/init/AppRunner.ts | 2 +- test/unit/init/AppRunner.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index c326252f26..9953f32942 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -5,7 +5,7 @@ import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; import yargs from 'yargs'; import { getLoggerFor } from '../logging/LogUtil'; -import { ensureTrailingSlash, resolveAssetPath } from '../util/PathUtil'; +import { resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; import type { VarResolver } from './VarResolver'; import { baseYargsOptions } from './VarResolver'; diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index 66384a01e8..4d6963c177 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -84,7 +84,7 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script' ], }); - expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'info', @@ -128,7 +128,7 @@ describe('AppRunner', (): void => { ], }); - expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'debug', @@ -168,7 +168,7 @@ describe('AppRunner', (): void => { ], }); - expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'debug', @@ -223,7 +223,7 @@ describe('AppRunner', (): void => { await new AppRunner().runCli(); - expect(ComponentsManager.build).toHaveBeenCalledTimes((1)); + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, logLevel: 'debug', @@ -255,10 +255,10 @@ describe('AppRunner', (): void => { argv: [ 'node', 'script' ], }); - expect(write).toHaveBeenCalledTimes((1)); + expect(write).toHaveBeenCalledTimes(1); expect(write).toHaveBeenNthCalledWith(1, expect.stringMatching(/^Error: could not instantiate server from .*default\.json/u)); - expect(write).toHaveBeenNthCalledWith((1), + expect(write).toHaveBeenNthCalledWith(1, expect.stringMatching(/^Error: Fatal/u)); expect(exit).toHaveBeenCalledTimes(1); From ab0f3ec37b6da327339a36e06ea9016b22374381 Mon Sep 17 00:00:00 2001 From: damooo Date: Wed, 17 Nov 2021 15:08:34 +0530 Subject: [PATCH 04/18] fix: (AppRunner) runCli method made sync --- config/app-setup/vars.json | 6 --- src/init/AppRunner.ts | 70 +++++++++++++++++------------ test/unit/init/AppRunner.test.ts | 77 ++++++++++++++++++++++++++------ 3 files changed, 105 insertions(+), 48 deletions(-) diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json index b6376136c7..31b9b648c7 100644 --- a/config/app-setup/vars.json +++ b/config/app-setup/vars.json @@ -11,14 +11,12 @@ "opts_strictMode": true, "opts_yargsOptions": [ { - "@type": "IYargsOptions", "VarResolver:_opts_yargsOptions_key": "baseUrl", "VarResolver:_opts_alias": "b", "VarResolver:_opts_requiresArg": true, "VarResolver:_opts_type": "string" }, { - "@type": "IYargsOptions", "VarResolver:_opts_yargsOptions_key": "port", "VarResolver:_opts_alias": "p", "VarResolver:_opts_requiresArg": true, @@ -26,7 +24,6 @@ "VarResolver:_opts_default": 3000 }, { - "@type": "IYargsOptions", "VarResolver:_opts_yargsOptions_key": "rootFilePath", "VarResolver:_opts_alias": "f", "VarResolver:_opts_requiresArg": true, @@ -34,21 +31,18 @@ "VarResolver:_opts_default": "./" }, { - "@type": "IYargsOptions", "VarResolver:_opts_yargsOptions_key": "showStackTrace", "VarResolver:_opts_alias": "t", "VarResolver:_opts_type": "boolean", "VarResolver:_opts_default": false }, { - "@type": "IYargsOptions", "VarResolver:_opts_yargsOptions_key": "sparqlEndpoint", "VarResolver:_opts_alias": "s", "VarResolver:_opts_requiresArg": true, "VarResolver:_opts_type": "string" }, { - "@type": "IYargsOptions", "VarResolver:_opts_yargsOptions_key": "podConfigJson", "VarResolver:_opts_requiresArg": true, "VarResolver:_opts_type": "string", diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 9953f32942..e613749d6f 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -13,16 +13,6 @@ import { baseYargsOptions } from './VarResolver'; const varConfigComponentIri = 'urn:solid-server-app-setup:default:VarResolver'; const appComponentIri = 'urn:solid-server:default:App'; -export interface CliParams { - loggingLevel: string; - port: number; - baseUrl?: string; - rootFilePath?: string; - sparqlEndpoint?: string; - showStackTrace?: boolean; - podConfigJson?: string; -} - export class AppRunner { private readonly logger = getLoggerFor(this); @@ -36,7 +26,7 @@ export class AppRunner { public async run( loaderProperties: IComponentsManagerBuilderOptions, configFile: string, - variableParams: CliParams, + variableParams: Record, ): Promise { const app = await this.createComponent(loaderProperties, configFile, variableParams, appComponentIri); await app.start(); @@ -48,7 +38,7 @@ export class AppRunner { * @param args - Command line arguments. * @param stderr - Standard error stream. */ - public async runCli({ + public runCli({ argv = process.argv, stderr = process.stderr, }: { @@ -56,12 +46,13 @@ export class AppRunner { stdin?: ReadStream; stdout?: WriteStream; stderr?: WriteStream; - } = {}): Promise { + } = {}): void { // Parse the command-line arguments - const params = await yargs(argv.slice(2)) + // eslint-disable-next-line no-sync + const params = yargs(argv.slice(2)) .usage('node ./bin/server.js [args]') .options(baseYargsOptions) - .parse(); + .parseSync(); // Gather settings for instantiating the server const loaderProperties = { @@ -72,36 +63,57 @@ export class AppRunner { // Create varResolver const varConfigFile = resolveAssetPath(params.varConfig as string); - const varResolver = await this.createComponent( + this.createComponent( loaderProperties as IComponentsManagerBuilderOptions, varConfigFile, {}, varConfigComponentIri, - ); - // Resolve vars for app startup - const vars = await varResolver.resolveVars(argv); + ).then( + // Using varResolver resolve vars and start app + async(varResolver): Promise => { + let vars; + try { + vars = await varResolver.resolveVars(argv); + } catch (error: unknown) { + this.exitWithError(error as Error, 'Error in computing variables', stderr); + } + return this.initApp( + loaderProperties, resolveAssetPath(params.config as string), vars as unknown as Record, stderr, + ); + }, - const configFile = resolveAssetPath(params.config as string); - let app: App; + (error): void => this.exitWithError( + error, `Error in loading variable configuration from ${varConfigFile}\n`, stderr, + ), + ).catch((error): void => this.exitWithError(error, 'Could not start the server', stderr)); + } + private exitWithError(error: Error, message: string, stderr: WriteStream): void { + stderr.write(message); + stderr.write(`${error.stack}\n`); + process.exit(1); + } + + private async initApp( + loaderProperties: IComponentsManagerBuilderOptions, + configFile: string, vars: Record, stderr: WriteStream, + ): Promise { + let app; // Create app try { app = await this.createComponent( - loaderProperties as IComponentsManagerBuilderOptions, configFile, vars, appComponentIri, + loaderProperties, configFile, vars, appComponentIri, ); } catch (error: unknown) { - stderr.write(`Error: could not instantiate server from ${configFile}\n`); - stderr.write(`${(error as Error).stack}\n`); - process.exit(1); + this.exitWithError(error as Error, `Error: could not instantiate server from ${configFile}\n`, stderr); } // Execute app try { - await app.start(); + await (app as unknown as App).start(); } catch (error: unknown) { - this.logger.error(`Could not start server: ${error}`, { error }); - process.exit(1); + this.exitWithError(error as Error, 'Could not start server', stderr); } } - public async createComponent( + private async createComponent( loaderProperties: IComponentsManagerBuilderOptions, configFile: string, variables: Record, diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index 4d6963c177..ec206e4df7 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -80,10 +80,15 @@ describe('AppRunner', (): void => { describe('runCli', (): void => { it('starts the server with default settings.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script' ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, @@ -113,7 +118,7 @@ describe('AppRunner', (): void => { }); it('accepts abbreviated flags.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script', '-b', 'http://pod.example/', @@ -128,6 +133,11 @@ describe('AppRunner', (): void => { ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, @@ -153,7 +163,7 @@ describe('AppRunner', (): void => { }); it('accepts full flags.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script', '--baseUrl', 'http://pod.example/', @@ -168,6 +178,11 @@ describe('AppRunner', (): void => { ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ dumpErrorState: true, @@ -193,12 +208,13 @@ describe('AppRunner', (): void => { }); it('accepts asset paths for the config flag.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script', '--config', '@css:config/file.json', ], }); + await new Promise(setImmediate); expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register).toHaveBeenCalledWith( @@ -221,7 +237,12 @@ describe('AppRunner', (): void => { '--podConfigJson', '/different-path.json', ]; - await new AppRunner().runCli(); + new AppRunner().runCli(); + + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); expect(ComponentsManager.build).toHaveBeenCalledTimes(1); expect(ComponentsManager.build).toHaveBeenCalledWith({ @@ -251,14 +272,19 @@ describe('AppRunner', (): void => { it('exits with output to stderr when instantiation fails.', async(): Promise => { manager.instantiate.mockRejectedValueOnce(new Error('Fatal')); - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script' ], }); - expect(write).toHaveBeenCalledTimes(1); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + + expect(write).toHaveBeenCalledTimes(2); expect(write).toHaveBeenNthCalledWith(1, expect.stringMatching(/^Error: could not instantiate server from .*default\.json/u)); - expect(write).toHaveBeenNthCalledWith(1, + expect(write).toHaveBeenNthCalledWith(2, expect.stringMatching(/^Error: Fatal/u)); expect(exit).toHaveBeenCalledTimes(1); @@ -267,50 +293,75 @@ describe('AppRunner', (): void => { it('exits without output to stderr when initialization fails.', async(): Promise => { app.start.mockRejectedValueOnce(new Error('Fatal')); - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script' ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(write).toHaveBeenCalledTimes(0); expect(exit).toHaveBeenCalledWith(1); }); it('exits when unknown options are passed to the main executable.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script', '--foo' ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(error).toHaveBeenCalledWith('Unknown argument: foo'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); it('exits when no value is passed to the main executable for an argument.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script', '-s' ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(error).toHaveBeenCalledWith('Not enough arguments following: s'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); it('exits when unknown parameters are passed to the main executable.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script', 'foo', 'bar', 'foo.txt', 'bar.txt' ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(error).toHaveBeenCalledWith('Unsupported positional arguments: "foo", "bar", "foo.txt", "bar.txt"'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); it('exits when multiple values for a parameter are passed.', async(): Promise => { - await new AppRunner().runCli({ + new AppRunner().runCli({ argv: [ 'node', 'script', '-l', 'info', '-l', 'debug' ], }); + // Wait until app.start has been called, because we can't await AppRunner.run. + await new Promise((resolve): void => { + setImmediate(resolve); + }); + expect(error).toHaveBeenCalledWith('Multiple values were provided for: "l": "info", "debug"'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); From 067a0e650503cf9c99afc3bd448a9f1bd1c687ce Mon Sep 17 00:00:00 2001 From: damooo Date: Fri, 19 Nov 2021 09:19:55 +0530 Subject: [PATCH 05/18] fix; (VarResolver) refactored to multiple files, and other stylistic fixes. --- config/app-setup/vars.json | 98 ++++++------ src/index.ts | 8 +- src/init/AppRunner.ts | 32 ++-- src/init/VarResolver.ts | 139 ------------------ src/init/variables/VarComputer.ts | 8 + src/init/variables/VarResolver.ts | 103 +++++++++++++ src/init/variables/computers/ArgExtractor.ts | 18 +++ .../variables/computers/AssetPathResolver.ts | 20 +++ .../variables/computers/BaseUrlResolver.ts | 12 ++ 9 files changed, 230 insertions(+), 208 deletions(-) delete mode 100644 src/init/VarResolver.ts create mode 100644 src/init/variables/VarComputer.ts create mode 100644 src/init/variables/VarResolver.ts create mode 100644 src/init/variables/computers/ArgExtractor.ts create mode 100644 src/init/variables/computers/AssetPathResolver.ts create mode 100644 src/init/variables/computers/BaseUrlResolver.ts diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json index 31b9b648c7..32780b5b7f 100644 --- a/config/app-setup/vars.json +++ b/config/app-setup/vars.json @@ -7,93 +7,89 @@ "comment": "VarResolver resolves variables to be used in app instantiation, from cli args", "@id": "urn:solid-server-app-setup:default:VarResolver", "@type": "VarResolver", - "opts_usage": "node ./bin/server.js [args]", - "opts_strictMode": true, - "opts_yargsOptions": [ - { - "VarResolver:_opts_yargsOptions_key": "baseUrl", - "VarResolver:_opts_alias": "b", - "VarResolver:_opts_requiresArg": true, - "VarResolver:_opts_type": "string" + "yargsArgOptions": { + "baseUrl": { + "alias": "b", + "requiresArg": true, + "type": "string" }, - { - "VarResolver:_opts_yargsOptions_key": "port", - "VarResolver:_opts_alias": "p", - "VarResolver:_opts_requiresArg": true, - "VarResolver:_opts_type": "number", - "VarResolver:_opts_default": 3000 + "port": { + "alias": "p", + "requiresArg": true, + "type": "number", + "default": 3000 }, - { - "VarResolver:_opts_yargsOptions_key": "rootFilePath", - "VarResolver:_opts_alias": "f", - "VarResolver:_opts_requiresArg": true, - "VarResolver:_opts_type": "string", - "VarResolver:_opts_default": "./" + "rootFilePath": { + "alias": "f", + "requiresArg": true, + "type": "string", + "default": "./" }, - { - "VarResolver:_opts_yargsOptions_key": "showStackTrace", - "VarResolver:_opts_alias": "t", - "VarResolver:_opts_type": "boolean", - "VarResolver:_opts_default": false + "showStackTrace": { + "alias": "t", + "type": "boolean", + "default": false }, - { - "VarResolver:_opts_yargsOptions_key": "sparqlEndpoint", - "VarResolver:_opts_alias": "s", - "VarResolver:_opts_requiresArg": true, - "VarResolver:_opts_type": "string" + "sparqlEndpoint": { + "alias": "s", + "requiresArg": true, + "type": "string" }, - { - "VarResolver:_opts_yargsOptions_key": "podConfigJson", - "VarResolver:_opts_requiresArg": true, - "VarResolver:_opts_type": "string", - "VarResolver:_opts_default": "./pod-config.json" + "podConfigJson": { + "requiresArg": true, + "type": "string", + "default": "./pod-config.json" } - ], - "opts_varComputers": [ + }, + "yargvOptions": { + "usage": "node ./bin/server.js [args]", + "strictMode": true + }, + "varComputers": [ { - "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:baseUrl", - "VarResolver:_opts_varComputers_value": { + "VarResolver:_varComputers_key": "urn:solid-server:default:variable:baseUrl", + "VarResolver:_varComputers_value": { "@type": "BaseUrlComputer" } }, { - "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:loggingLevel", - "VarResolver:_opts_varComputers_value": { + "VarResolver:_varComputers_key": "urn:solid-server:default:variable:loggingLevel", + "VarResolver:_varComputers_value": { "@type": "ArgExtractor", "argKey": "loggingLevel" } }, { - "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:port", - "VarResolver:_opts_varComputers_value": { + "VarResolver:_varComputers_key": "urn:solid-server:default:variable:port", + "VarResolver:_varComputers_value": { "@type": "ArgExtractor", "argKey": "port" } }, { - "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:rootFilePath", - "VarResolver:_opts_varComputers_value": { + "VarResolver:_varComputers_key": "urn:solid-server:default:variable:rootFilePath", + "VarResolver:_varComputers_value": { "@type": "AssetPathResolver", "pathArgKey": "rootFilePath" } }, { - "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:sparqlEndpoint", - "VarResolver:_opts_varComputers_value": { + "VarResolver:_varComputers_key": "urn:solid-server:default:variable:sparqlEndpoint", + "VarResolver:_varComputers_value": { "@type": "ArgExtractor", "argKey": "sparqlEndpoint" } }, { - "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:showStackTrace", - "VarResolver:_opts_varComputers_value": { + "VarResolver:_varComputers_key": "urn:solid-server:default:variable:showStackTrace", + "VarResolver:_varComputers_value": { "@type": "ArgExtractor", "argKey": "showStackTrace" } }, { - "VarResolver:_opts_varComputers_key": "urn:solid-server:default:variable:AssetPathResolver", - "VarResolver:_opts_varComputers_value": { + "VarResolver:_varComputers_key": "urn:solid-server:default:variable:AssetPathResolver", + "VarResolver:_varComputers_value": { "@type": "AssetPathResolver", "pathArgKey": "podConfigJson" } diff --git a/src/index.ts b/src/index.ts index 4dc4fce9dc..ce1eb8198d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -185,7 +185,13 @@ export * from './init/ContainerInitializer'; export * from './init/Initializer'; export * from './init/LoggerInitializer'; export * from './init/ServerInitializer'; -export * from './init/VarResolver'; + +// Init/Vars +export * from './init/variables/VarResolver'; +export * from './init/variables/VarComputer'; +export * from './init/variables/computers/ArgExtractor'; +export * from './init/variables/computers/AssetPathResolver'; +export * from './init/variables/computers/BaseUrlResolver'; // Logging export * from './logging/LazyLogger'; diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index e613749d6f..7f8250398c 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -7,8 +7,8 @@ import yargs from 'yargs'; import { getLoggerFor } from '../logging/LogUtil'; import { resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; -import type { VarResolver } from './VarResolver'; -import { baseYargsOptions } from './VarResolver'; +import type { VarResolver, VarRecord } from './variables/VarResolver'; +import { BASE_YARGS_ARG_OPTIONS } from './variables/VarResolver'; const varConfigComponentIri = 'urn:solid-server-app-setup:default:VarResolver'; const appComponentIri = 'urn:solid-server:default:App'; @@ -26,7 +26,7 @@ export class AppRunner { public async run( loaderProperties: IComponentsManagerBuilderOptions, configFile: string, - variableParams: Record, + variableParams: VarRecord, ): Promise { const app = await this.createComponent(loaderProperties, configFile, variableParams, appComponentIri); await app.start(); @@ -51,10 +51,10 @@ export class AppRunner { // eslint-disable-next-line no-sync const params = yargs(argv.slice(2)) .usage('node ./bin/server.js [args]') - .options(baseYargsOptions) + .options(BASE_YARGS_ARG_OPTIONS) .parseSync(); - // Gather settings for instantiating the server + // Gather settings for instantiating VarResolver const loaderProperties = { mainModulePath: resolveAssetPath(params.mainModulePath as string), dumpErrorState: true, @@ -68,9 +68,9 @@ export class AppRunner { ).then( // Using varResolver resolve vars and start app async(varResolver): Promise => { - let vars; + let vars: VarRecord; try { - vars = await varResolver.resolveVars(argv); + vars = await varResolver.handle(argv); } catch (error: unknown) { this.exitWithError(error as Error, 'Error in computing variables', stderr); } @@ -80,13 +80,13 @@ export class AppRunner { }, (error): void => this.exitWithError( - error, `Error in loading variable configuration from ${varConfigFile}\n`, stderr, + error, `Error in loading variable configuration from ${varConfigFile}`, stderr, ), ).catch((error): void => this.exitWithError(error, 'Could not start the server', stderr)); } - private exitWithError(error: Error, message: string, stderr: WriteStream): void { - stderr.write(message); + private exitWithError(error: Error, message: string, stderr: WriteStream): never { + stderr.write(`message\n`); stderr.write(`${error.stack}\n`); process.exit(1); } @@ -95,19 +95,17 @@ export class AppRunner { loaderProperties: IComponentsManagerBuilderOptions, configFile: string, vars: Record, stderr: WriteStream, ): Promise { - let app; + let app: App; // Create app try { - app = await this.createComponent( - loaderProperties, configFile, vars, appComponentIri, - ); + app = await this.createComponent(loaderProperties, configFile, vars, appComponentIri); } catch (error: unknown) { - this.exitWithError(error as Error, `Error: could not instantiate server from ${configFile}\n`, stderr); + this.exitWithError(error as Error, `Error: could not instantiate server from ${configFile}`, stderr); } // Execute app try { - await (app as unknown as App).start(); + await app.start(); } catch (error: unknown) { this.exitWithError(error as Error, 'Could not start server', stderr); } @@ -123,7 +121,7 @@ export class AppRunner { const componentsManager = await ComponentsManager.build(loaderProperties); await componentsManager.configRegistry.register(configFile); - // Create the app + // Create the component return await componentsManager.instantiate(componentIri, { variables }); } } diff --git a/src/init/VarResolver.ts b/src/init/VarResolver.ts deleted file mode 100644 index b98d42dab7..0000000000 --- a/src/init/VarResolver.ts +++ /dev/null @@ -1,139 +0,0 @@ -import yargs from 'yargs'; -import { AsyncHandler } from '../util/handlers/AsyncHandler'; -import { ensureTrailingSlash, modulePathPlaceholder, resolveAssetPath } from '../util/PathUtil'; - -const defaultConfig = `${modulePathPlaceholder}config/default.json`; -const defaultVarConfig = `${modulePathPlaceholder}config/app-setup/vars.json`; - -/** - * CLI options needed for performing meta-process of app initialization. - * These options doesn't contribute to components-js vars normally. - */ -export const baseYargsOptions: Record = { - config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, - loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true }, - mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, - varConfig: { type: 'string', alias: 'v', default: defaultVarConfig, requiresArg: true }, -}; - -/** - * A handler that takes args, and returns computed variable value - */ -export abstract class VarComputer extends AsyncHandler { - public abstract handle(args: yargs.Arguments): Promise; -} - -// Currently componentsjs-builder giving error, if we use external TSTypeReference. -// Re-definition is temporary work around. -export interface IYargsOptions extends yargs.Options { - alias?: string | readonly string[]; - default?: any; - desc?: string; - requiresArg?: boolean; - type?: 'array' | 'count' | yargs.PositionalOptionsType; -} - -export interface IVarResolverOptions { - yargsOptions: Record; - varComputers: Record; - usage?: string; - strictMode?: boolean; -} - -export class VarResolver { - protected readonly opts: IVarResolverOptions; - protected readonly effectiveYargsOptions: Record; - - public constructor(opts: IVarResolverOptions) { - this.opts = opts; - this.effectiveYargsOptions = { - ...opts.yargsOptions, - ...baseYargsOptions, - }; - } - - private async parseArgs(argv: readonly string[]): Promise { - let yArgv = yargs(argv.slice(2)); - if (this.opts.usage !== undefined) { - yArgv = yArgv.usage(this.opts.usage); - } - if (this.opts.strictMode) { - yArgv = yArgv.strict(); - } - - yArgv = yArgv.check((args): boolean => { - if (args._.length > 0) { - throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); - } - for (const key of Object.keys(args)) { - const val = args[key]; - if (key !== '_' && Array.isArray(val)) { - throw new Error(`Multiple values were provided for: "${key}": "${val.join('", "')}"`); - } - } - return true; - }).options(this.effectiveYargsOptions); - - return yArgv.parse(); - } - - private async computeVars(args: yargs.Arguments): Promise> { - const vars: Record = {}; - for (const varId of Object.keys(this.opts.varComputers)) { - const varComputer = this.opts.varComputers[varId]; - try { - vars[varId] = await varComputer.handle(args); - } catch (err: unknown) { - throw new Error(`Error in computing value for variable ${varId}: ${err}`); - } - } - return vars; - } - - public async resolveVars(argv: readonly string[]): Promise> { - const args = await this.parseArgs(argv); - return this.computeVars(args); - } -} - -/** - * Simple VarComputer that just extracts an arg from parsed args. - */ -export class ArgExtractor extends VarComputer { - private readonly argKey: string; - - public constructor(argKey: string) { - super(); - this.argKey = argKey; - } - - public async handle(args: yargs.Arguments): Promise { - return args[this.argKey]; - } -} - -export class AssetPathResolver extends VarComputer { - private readonly pathArgKey: string; - - public constructor(pathArgKey: string) { - super(); - this.pathArgKey = pathArgKey; - } - - public async handle(args: yargs.Arguments): Promise { - const path = args[this.pathArgKey]; - if (typeof path !== 'string') { - throw new Error(`Invalid ${this.pathArgKey} argument`); - } - return resolveAssetPath(path); - } -} - -/** - * A handler to compute base-url from args - */ -export class BaseUrlComputer extends VarComputer { - public async handle(args: yargs.Arguments): Promise { - return args.baseUrl ? ensureTrailingSlash(args.baseUrl as string) : `http://localhost:${args.port}/`; - } -} diff --git a/src/init/variables/VarComputer.ts b/src/init/variables/VarComputer.ts new file mode 100644 index 0000000000..6e523ecc48 --- /dev/null +++ b/src/init/variables/VarComputer.ts @@ -0,0 +1,8 @@ +import type yargs from 'yargs'; +import { AsyncHandler } from '../../util/handlers/AsyncHandler'; + +/** + * A handler that takes args, and returns computed variable value + */ +export abstract class VarComputer extends AsyncHandler { +} diff --git a/src/init/variables/VarResolver.ts b/src/init/variables/VarResolver.ts new file mode 100644 index 0000000000..58bf2ca5d1 --- /dev/null +++ b/src/init/variables/VarResolver.ts @@ -0,0 +1,103 @@ +/* eslint-disable tsdoc/syntax */ +import yargs from 'yargs'; +import { createErrorMessage } from '../..'; +import { AsyncHandler } from '../../util/handlers/AsyncHandler'; +import { modulePathPlaceholder } from '../../util/PathUtil'; +import type { VarComputer } from './VarComputer'; + +const defaultConfig = `${modulePathPlaceholder}config/default.json`; +const defaultVarConfig = `${modulePathPlaceholder}config/app-setup/vars.json`; + +export type YargsArgOptions = Record; + +/** + * CLI options needed for performing meta-process of app initialization. + * These options doesn't contribute to components-js vars normally. + */ +export const BASE_YARGS_ARG_OPTIONS: YargsArgOptions = { + config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, + loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true }, + mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, + varConfig: { type: 'string', alias: 'v', default: defaultVarConfig, requiresArg: true }, +}; + +export interface YargvOptions { + usage?: string; + strictMode?: boolean; + // @see http://yargs.js.org/docs/#api-reference-envprefix + loadFromEnv?: boolean; + envVarPrefix?: string; +} + +export type VarRecord = Record; + +export class VarResolver extends AsyncHandler { + protected readonly yargsArgOptions: YargsArgOptions; + protected readonly effectiveYargsArgOptions: YargsArgOptions; + protected readonly yargvOptions: YargvOptions; + protected readonly varComputers: Record; + + /** + * @param yargsArgOptions - record of option to it's yargs opt config. @range {json} + * @param yargvOptions - options to configure yargv. @range {json} + * @param varComputers - record of componentsjs var-iri to VarComputer. + */ + public constructor( + yargsArgOptions: YargsArgOptions, yargvOptions: YargvOptions, varComputers: Record, + ) { + super(); + this.yargsArgOptions = yargsArgOptions; + this.effectiveYargsArgOptions = { + ...yargsArgOptions, + ...BASE_YARGS_ARG_OPTIONS, + }; + this.yargvOptions = yargvOptions; + this.varComputers = varComputers; + } + + private async parseArgs(argv: readonly string[]): Promise { + let yArgv = yargs(argv.slice(2)); + if (this.yargvOptions.usage !== undefined) { + yArgv = yArgv.usage(this.yargvOptions.usage); + } + if (this.yargvOptions.strictMode) { + yArgv = yArgv.strict(); + } + if (this.yargvOptions.loadFromEnv) { + yArgv = yArgv.env(this.yargvOptions.envVarPrefix ?? ''); + } + + yArgv = yArgv.check((args): boolean => { + if (args._.length > 0) { + throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); + } + for (const [ key, val ] of Object.entries(args)) { + // We have no options that allow for arrays + if (key !== '_' && Array.isArray(val)) { + throw new Error(`Multiple values were provided for: "${key}": "${val.join('", "')}"`); + } + } + return true; + }).options(this.effectiveYargsArgOptions); + + return yArgv.parse(); + } + + private async computeVars(args: yargs.Arguments): Promise> { + const vars: Record = {}; + for (const varId of Object.keys(this.varComputers)) { + const varComputer = this.varComputers[varId]; + try { + vars[varId] = await varComputer.handle(args); + } catch (err: unknown) { + throw new Error(`Error in computing value for variable ${varId}: ${createErrorMessage(err)}`); + } + } + return vars; + } + + public async handle(argv: readonly string[]): Promise { + const args = await this.parseArgs(argv); + return this.computeVars(args); + } +} diff --git a/src/init/variables/computers/ArgExtractor.ts b/src/init/variables/computers/ArgExtractor.ts new file mode 100644 index 0000000000..50d852e7ff --- /dev/null +++ b/src/init/variables/computers/ArgExtractor.ts @@ -0,0 +1,18 @@ +import type yargs from 'yargs'; +import { VarComputer } from '../VarComputer'; + +/** + * Simple VarComputer that just extracts an arg from parsed args. + */ +export class ArgExtractor extends VarComputer { + private readonly argKey: string; + + public constructor(argKey: string) { + super(); + this.argKey = argKey; + } + + public async handle(args: yargs.Arguments): Promise { + return args[this.argKey]; + } +} diff --git a/src/init/variables/computers/AssetPathResolver.ts b/src/init/variables/computers/AssetPathResolver.ts new file mode 100644 index 0000000000..73a9977dec --- /dev/null +++ b/src/init/variables/computers/AssetPathResolver.ts @@ -0,0 +1,20 @@ +import type yargs from 'yargs'; +import { resolveAssetPath } from '../../..'; +import { VarComputer } from '../VarComputer'; + +export class AssetPathResolver extends VarComputer { + private readonly pathArgKey: string; + + public constructor(pathArgKey: string) { + super(); + this.pathArgKey = pathArgKey; + } + + public async handle(args: yargs.Arguments): Promise { + const path = args[this.pathArgKey]; + if (typeof path !== 'string') { + throw new Error(`Invalid ${this.pathArgKey} argument`); + } + return resolveAssetPath(path); + } +} diff --git a/src/init/variables/computers/BaseUrlResolver.ts b/src/init/variables/computers/BaseUrlResolver.ts new file mode 100644 index 0000000000..615b779fcc --- /dev/null +++ b/src/init/variables/computers/BaseUrlResolver.ts @@ -0,0 +1,12 @@ +import type yargs from 'yargs'; +import { ensureTrailingSlash } from '../../..'; +import { VarComputer } from '../VarComputer'; + +/** + * A handler to compute base-url from args + */ +export class BaseUrlComputer extends VarComputer { + public async handle(args: yargs.Arguments): Promise { + return args.baseUrl ? ensureTrailingSlash(args.baseUrl as string) : `http://localhost:${args.port}/`; + } +} From 72295287e71371fe907d1222fa7b0f23686295eb Mon Sep 17 00:00:00 2001 From: damooo Date: Thu, 25 Nov 2021 14:22:42 +0530 Subject: [PATCH 06/18] chore: (AppRunner) Uses builder pattern for yargs base arguments setup to enable better typescript inference --- src/init/AppRunner.ts | 15 +++++++------- src/init/variables/VarResolver.ts | 33 ++++++++++++++++--------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 7f8250398c..07c97877fb 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -4,11 +4,12 @@ import type { ReadStream, WriteStream } from 'tty'; import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; import yargs from 'yargs'; + import { getLoggerFor } from '../logging/LogUtil'; import { resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; import type { VarResolver, VarRecord } from './variables/VarResolver'; -import { BASE_YARGS_ARG_OPTIONS } from './variables/VarResolver'; +import { setupYargvWithBaseArgs } from './variables/VarResolver'; const varConfigComponentIri = 'urn:solid-server-app-setup:default:VarResolver'; const appComponentIri = 'urn:solid-server:default:App'; @@ -49,20 +50,18 @@ export class AppRunner { } = {}): void { // Parse the command-line arguments // eslint-disable-next-line no-sync - const params = yargs(argv.slice(2)) - .usage('node ./bin/server.js [args]') - .options(BASE_YARGS_ARG_OPTIONS) - .parseSync(); + const params = setupYargvWithBaseArgs(yargs(argv.slice(2)) + .usage('node ./bin/server.js [args]')).parseSync(); // Gather settings for instantiating VarResolver const loaderProperties = { - mainModulePath: resolveAssetPath(params.mainModulePath as string), + mainModulePath: resolveAssetPath(params.mainModulePath), dumpErrorState: true, logLevel: params.loggingLevel as LogLevel, }; // Create varResolver - const varConfigFile = resolveAssetPath(params.varConfig as string); + const varConfigFile = resolveAssetPath(params.varConfig); this.createComponent( loaderProperties as IComponentsManagerBuilderOptions, varConfigFile, {}, varConfigComponentIri, ).then( @@ -75,7 +74,7 @@ export class AppRunner { this.exitWithError(error as Error, 'Error in computing variables', stderr); } return this.initApp( - loaderProperties, resolveAssetPath(params.config as string), vars as unknown as Record, stderr, + loaderProperties, resolveAssetPath(params.config), vars as unknown as Record, stderr, ); }, diff --git a/src/init/variables/VarResolver.ts b/src/init/variables/VarResolver.ts index 58bf2ca5d1..d4ae3c9e86 100644 --- a/src/init/variables/VarResolver.ts +++ b/src/init/variables/VarResolver.ts @@ -10,16 +10,21 @@ const defaultVarConfig = `${modulePathPlaceholder}config/app-setup/vars.json`; export type YargsArgOptions = Record; -/** - * CLI options needed for performing meta-process of app initialization. - * These options doesn't contribute to components-js vars normally. - */ -export const BASE_YARGS_ARG_OPTIONS: YargsArgOptions = { - config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, - loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true }, - mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, - varConfig: { type: 'string', alias: 'v', default: defaultVarConfig, requiresArg: true }, -}; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function setupYargvWithBaseArgs(yargv: yargs.Argv) { + // Values from componentsjs LogLevel type + const logLevels = [ 'error', 'warn', 'info', 'verbose', 'debug', 'silly' ]; + return yargv.options({ + /** + * CLI options needed for performing meta-process of app initialization. + * These options doesn't contribute to components-js vars normally. + */ + config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, + loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: logLevels }, + mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, + varConfig: { type: 'string', alias: 'v', default: defaultVarConfig, requiresArg: true }, + }); +} export interface YargvOptions { usage?: string; @@ -33,7 +38,6 @@ export type VarRecord = Record; export class VarResolver extends AsyncHandler { protected readonly yargsArgOptions: YargsArgOptions; - protected readonly effectiveYargsArgOptions: YargsArgOptions; protected readonly yargvOptions: YargvOptions; protected readonly varComputers: Record; @@ -47,10 +51,6 @@ export class VarResolver extends AsyncHandler { ) { super(); this.yargsArgOptions = yargsArgOptions; - this.effectiveYargsArgOptions = { - ...yargsArgOptions, - ...BASE_YARGS_ARG_OPTIONS, - }; this.yargvOptions = yargvOptions; this.varComputers = varComputers; } @@ -78,7 +78,8 @@ export class VarResolver extends AsyncHandler { } } return true; - }).options(this.effectiveYargsArgOptions); + }).options(this.yargsArgOptions); + setupYargvWithBaseArgs(yArgv); return yArgv.parse(); } From 2d050318e9915c6ffaa58215558f91454fe0bb17 Mon Sep 17 00:00:00 2001 From: damooo Date: Sun, 5 Dec 2021 21:24:16 +0530 Subject: [PATCH 07/18] fix(AppRunner): refactoring AppRunner and VarResolver --- config/app-setup/vars.json | 6 +- package-lock.json | 63 ++++++++++++++++ package.json | 2 + src/init/AppRunner.ts | 66 ++++++++-------- src/init/variables/VarComputer.ts | 2 +- src/init/variables/VarResolver.ts | 75 +++++++++++-------- .../variables/computers/AssetPathResolver.ts | 4 + src/logging/LogLevel.ts | 2 + 8 files changed, 154 insertions(+), 66 deletions(-) diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json index 32780b5b7f..e3691cd109 100644 --- a/config/app-setup/vars.json +++ b/config/app-setup/vars.json @@ -7,7 +7,7 @@ "comment": "VarResolver resolves variables to be used in app instantiation, from cli args", "@id": "urn:solid-server-app-setup:default:VarResolver", "@type": "VarResolver", - "yargsArgOptions": { + "parameters": { "baseUrl": { "alias": "b", "requiresArg": true, @@ -41,8 +41,8 @@ "default": "./pod-config.json" } }, - "yargvOptions": { - "usage": "node ./bin/server.js [args]", + "options": { + "usage": "community-solid-server [args]", "strictMode": true }, "varComputers": [ diff --git a/package-lock.json b/package-lock.json index 14c2c49a95..ff47a6d50e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "sparqljs": "^3.4.2", "url-join": "^4.0.1", "uuid": "^8.3.2", + "verror": "^1.10.1", "winston": "^3.3.3", "winston-transport": "^4.4.0", "ws": "^8.2.3", @@ -78,6 +79,7 @@ "@types/jest": "^27.0.0", "@types/set-cookie-parser": "^2.4.0", "@types/supertest": "^2.0.11", + "@types/verror": "^1.10.5", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", "cheerio": "^1.0.0-rc.10", @@ -4951,6 +4953,12 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, + "node_modules/@types/verror": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz", + "integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", @@ -5476,6 +5484,14 @@ "node": ">=0.10.0" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -8151,6 +8167,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -15296,6 +15320,19 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vscode-textmate": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", @@ -19660,6 +19697,12 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, + "@types/verror": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz", + "integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==", + "dev": true + }, "@types/ws": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", @@ -20036,6 +20079,11 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -22055,6 +22103,11 @@ } } }, + "extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -27597,6 +27650,16 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "vscode-textmate": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", diff --git a/package.json b/package.json index 4580f267b2..e4ca4e95e6 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "sparqljs": "^3.4.2", "url-join": "^4.0.1", "uuid": "^8.3.2", + "verror": "^1.10.1", "winston": "^3.3.3", "winston-transport": "^4.4.0", "ws": "^8.2.3", @@ -141,6 +142,7 @@ "@types/jest": "^27.0.0", "@types/set-cookie-parser": "^2.4.0", "@types/supertest": "^2.0.11", + "@types/verror": "^1.10.5", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", "cheerio": "^1.0.0-rc.10", diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 07c97877fb..1d6d70e961 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -3,16 +3,16 @@ import type { ReadStream, WriteStream } from 'tty'; import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; +import VError from 'verror'; import yargs from 'yargs'; - import { getLoggerFor } from '../logging/LogUtil'; import { resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; -import type { VarResolver, VarRecord } from './variables/VarResolver'; -import { setupYargvWithBaseArgs } from './variables/VarResolver'; +import type { VarResolver, VariableValues } from './variables/VarResolver'; +import { setupBaseArgs } from './variables/VarResolver'; -const varConfigComponentIri = 'urn:solid-server-app-setup:default:VarResolver'; -const appComponentIri = 'urn:solid-server:default:App'; +const DEFAULT_VAR_RESOLVER = 'urn:solid-server-app-setup:default:VarResolver'; +const DEFAULT_APP = 'urn:solid-server:default:App'; export class AppRunner { private readonly logger = getLoggerFor(this); @@ -22,14 +22,14 @@ export class AppRunner { * This method can be used to start the server from within another JavaScript application. * @param loaderProperties - Components.js loader properties. * @param configFile - Path to the server config file. - * @param variableParams - Variables to pass into the config file. + * @param variables - Variables to pass into the config file. */ public async run( loaderProperties: IComponentsManagerBuilderOptions, configFile: string, - variableParams: VarRecord, + variables: VariableValues, ): Promise { - const app = await this.createComponent(loaderProperties, configFile, variableParams, appComponentIri); + const app = await this.createComponent(loaderProperties, configFile, variables, DEFAULT_APP); await app.start(); } @@ -49,9 +49,7 @@ export class AppRunner { stderr?: WriteStream; } = {}): void { // Parse the command-line arguments - // eslint-disable-next-line no-sync - const params = setupYargvWithBaseArgs(yargs(argv.slice(2)) - .usage('node ./bin/server.js [args]')).parseSync(); + const params = this.parseCliParams(argv); // Gather settings for instantiating VarResolver const loaderProperties = { @@ -60,57 +58,63 @@ export class AppRunner { logLevel: params.loggingLevel as LogLevel, }; - // Create varResolver + // Create a resolver, that resolves componentsjs variables from cli-params const varConfigFile = resolveAssetPath(params.varConfig); this.createComponent( - loaderProperties as IComponentsManagerBuilderOptions, varConfigFile, {}, varConfigComponentIri, + loaderProperties as IComponentsManagerBuilderOptions, varConfigFile, {}, DEFAULT_VAR_RESOLVER, ).then( // Using varResolver resolve vars and start app async(varResolver): Promise => { - let vars: VarRecord; + let vars: VariableValues; try { vars = await varResolver.handle(argv); } catch (error: unknown) { - this.exitWithError(error as Error, 'Error in computing variables', stderr); + throw new VError(error as Error, 'Error in computing variables'); } - return this.initApp( - loaderProperties, resolveAssetPath(params.config), vars as unknown as Record, stderr, + return this.startApp( + loaderProperties, resolveAssetPath(params.config), vars as unknown as Record, ); }, - (error): void => this.exitWithError( - error, `Error in loading variable configuration from ${varConfigFile}`, stderr, - ), - ).catch((error): void => this.exitWithError(error, 'Could not start the server', stderr)); + (error): void => { + throw new VError(error as Error, `Error in loading variable configuration from ${varConfigFile}`); + }, + ).catch((error): void => { + stderr.write(`Could not start the server\nCause:\n${error.message}\n`); + const stack = error instanceof VError ? error.cause()?.stack : error.stack; + stderr.write(`${stack}\n`); + process.exit(1); + }); } - private exitWithError(error: Error, message: string, stderr: WriteStream): never { - stderr.write(`message\n`); - stderr.write(`${error.stack}\n`); - process.exit(1); + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + private parseCliParams(argv: string[]) { + // eslint-disable-next-line no-sync + return setupBaseArgs(yargs(argv.slice(2)) + .usage('node ./bin/server.js [args]')).parseSync(); } - private async initApp( + private async startApp( loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, vars: Record, stderr: WriteStream, + configFile: string, vars: Record, ): Promise { let app: App; // Create app try { - app = await this.createComponent(loaderProperties, configFile, vars, appComponentIri); + app = await this.createComponent(loaderProperties, configFile, vars, DEFAULT_APP); } catch (error: unknown) { - this.exitWithError(error as Error, `Error: could not instantiate server from ${configFile}`, stderr); + throw new VError(error as Error, `Error: could not instantiate server from ${configFile}`); } // Execute app try { await app.start(); } catch (error: unknown) { - this.exitWithError(error as Error, 'Could not start server', stderr); + throw new VError(error as Error, 'Could not start server'); } } - private async createComponent( + public async createComponent( loaderProperties: IComponentsManagerBuilderOptions, configFile: string, variables: Record, diff --git a/src/init/variables/VarComputer.ts b/src/init/variables/VarComputer.ts index 6e523ecc48..2877ec2a0e 100644 --- a/src/init/variables/VarComputer.ts +++ b/src/init/variables/VarComputer.ts @@ -2,7 +2,7 @@ import type yargs from 'yargs'; import { AsyncHandler } from '../../util/handlers/AsyncHandler'; /** - * A handler that takes args, and returns computed variable value + A handler that determines the value of a specific variable from parsed cli arguments. */ export abstract class VarComputer extends AsyncHandler { } diff --git a/src/init/variables/VarResolver.ts b/src/init/variables/VarResolver.ts index d4ae3c9e86..cc78238ec5 100644 --- a/src/init/variables/VarResolver.ts +++ b/src/init/variables/VarResolver.ts @@ -1,6 +1,6 @@ /* eslint-disable tsdoc/syntax */ import yargs from 'yargs'; -import { createErrorMessage } from '../..'; +import { createErrorMessage, LOG_LEVELS } from '../..'; import { AsyncHandler } from '../../util/handlers/AsyncHandler'; import { modulePathPlaceholder } from '../../util/PathUtil'; import type { VarComputer } from './VarComputer'; @@ -11,51 +11,65 @@ const defaultVarConfig = `${modulePathPlaceholder}config/app-setup/vars.json`; export type YargsArgOptions = Record; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function setupYargvWithBaseArgs(yargv: yargs.Argv) { - // Values from componentsjs LogLevel type - const logLevels = [ 'error', 'warn', 'info', 'verbose', 'debug', 'silly' ]; +export function setupBaseArgs(yargv: yargs.Argv) { return yargv.options({ /** - * CLI options needed for performing meta-process of app initialization. - * These options doesn't contribute to components-js vars normally. - */ + * CLI options needed for performing meta-process of app initialization. + * These options doesn't contribute to components-js vars normally. + */ config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, - loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: logLevels }, + loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS }, mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, varConfig: { type: 'string', alias: 'v', default: defaultVarConfig, requiresArg: true }, }); } -export interface YargvOptions { +export interface CliOptions { + // Usage string to be given at cli usage?: string; + // StrictMode determines wether to allow undefined cli-args or not. strictMode?: boolean; + // Wether to load arguments from env-vars or not. // @see http://yargs.js.org/docs/#api-reference-envprefix loadFromEnv?: boolean; + // Prefix for env-vars. + // see yargv docs for behavior. http://yargs.js.org/docs/#api-reference-envprefix envVarPrefix?: string; } -export type VarRecord = Record; +export type VariableValues = Record; -export class VarResolver extends AsyncHandler { +/** + * This class translates command-line arguments/env-vars into values for specific variables, + * which can then be used to instantiate a parametrized Components.js configuration. + */ +export class VarResolver extends AsyncHandler { protected readonly yargsArgOptions: YargsArgOptions; - protected readonly yargvOptions: YargvOptions; + protected readonly yargvOptions: CliOptions; protected readonly varComputers: Record; /** - * @param yargsArgOptions - record of option to it's yargs opt config. @range {json} - * @param yargvOptions - options to configure yargv. @range {json} - * @param varComputers - record of componentsjs var-iri to VarComputer. - */ + * @param parameters - record of option to it's yargs opt config. @range {json} + * @param options - options to configure yargv. @range {json} + * @param varComputers - record of componentsjs var-iri to VarComputer. + */ public constructor( - yargsArgOptions: YargsArgOptions, yargvOptions: YargvOptions, varComputers: Record, + parameters: YargsArgOptions, options: CliOptions, varComputers: Record, ) { super(); - this.yargsArgOptions = yargsArgOptions; - this.yargvOptions = yargvOptions; + this.yargsArgOptions = parameters; + this.yargvOptions = options; this.varComputers = varComputers; } private async parseArgs(argv: readonly string[]): Promise { + let yArgv = this.createYArgv(argv); + yArgv = this.validateArguments(yArgv); + + return yArgv.parse(); + } + + private createYArgv(argv: readonly string[]): yargs.Argv { let yArgv = yargs(argv.slice(2)); if (this.yargvOptions.usage !== undefined) { yArgv = yArgv.usage(this.yargvOptions.usage); @@ -66,8 +80,11 @@ export class VarResolver extends AsyncHandler { if (this.yargvOptions.loadFromEnv) { yArgv = yArgv.env(this.yargvOptions.envVarPrefix ?? ''); } + return setupBaseArgs(yArgv.options(this.yargsArgOptions)); + } - yArgv = yArgv.check((args): boolean => { + private validateArguments(yArgv: yargs.Argv): yargs.Argv { + return yArgv.check((args): boolean => { if (args._.length > 0) { throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); } @@ -78,27 +95,23 @@ export class VarResolver extends AsyncHandler { } } return true; - }).options(this.yargsArgOptions); - setupYargvWithBaseArgs(yArgv); - - return yArgv.parse(); + }); } - private async computeVars(args: yargs.Arguments): Promise> { + private async resolveVariables(args: yargs.Arguments): Promise> { const vars: Record = {}; - for (const varId of Object.keys(this.varComputers)) { - const varComputer = this.varComputers[varId]; + for (const [ name, computer ] of Object.entries(this.varComputers)) { try { - vars[varId] = await varComputer.handle(args); + vars[name] = await computer.handle(args); } catch (err: unknown) { - throw new Error(`Error in computing value for variable ${varId}: ${createErrorMessage(err)}`); + throw new Error(`Error in computing value for variable ${name}: ${createErrorMessage(err)}`); } } return vars; } - public async handle(argv: readonly string[]): Promise { + public async handle(argv: readonly string[]): Promise { const args = await this.parseArgs(argv); - return this.computeVars(args); + return this.resolveVariables(args); } } diff --git a/src/init/variables/computers/AssetPathResolver.ts b/src/init/variables/computers/AssetPathResolver.ts index 73a9977dec..f86a9be9d9 100644 --- a/src/init/variables/computers/AssetPathResolver.ts +++ b/src/init/variables/computers/AssetPathResolver.ts @@ -2,6 +2,10 @@ import type yargs from 'yargs'; import { resolveAssetPath } from '../../..'; import { VarComputer } from '../VarComputer'; +/** + * This `VarComputer` resolves absolute path of asset, from path specified in specified argument. + * It follows conventions of `resolveAssetPath` function for path resolution. + */ export class AssetPathResolver extends VarComputer { private readonly pathArgKey: string; diff --git a/src/logging/LogLevel.ts b/src/logging/LogLevel.ts index e67f9755ed..58b7f4ae5e 100644 --- a/src/logging/LogLevel.ts +++ b/src/logging/LogLevel.ts @@ -2,3 +2,5 @@ * Different log levels, from most important to least important. */ export type LogLevel = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly'; + +export const LOG_LEVELS = [ 'error', 'warn', 'info', 'verbose', 'debug', 'silly' ]; From 0b9d83ccc42b50b873c28550ef5b09a4f166b9f1 Mon Sep 17 00:00:00 2001 From: damooo Date: Sun, 5 Dec 2021 22:52:11 +0530 Subject: [PATCH 08/18] fix(AppRunner): refactoring AppRunner promise handling --- config/app-setup/vars.json | 2 +- src/init/AppRunner.ts | 85 ++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json index e3691cd109..3311dc5d45 100644 --- a/config/app-setup/vars.json +++ b/config/app-setup/vars.json @@ -42,7 +42,7 @@ } }, "options": { - "usage": "community-solid-server [args]", + "usage": "node ./bin/server.js [args]", "strictMode": true }, "varComputers": [ diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 1d6d70e961..064a135f04 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -49,7 +49,9 @@ export class AppRunner { stderr?: WriteStream; } = {}): void { // Parse the command-line arguments - const params = this.parseCliParams(argv); + // eslint-disable-next-line no-sync + const params = setupBaseArgs(yargs(argv.slice(2)) + .usage('node ./bin/server.js [args]')).parseSync(); // Gather settings for instantiating VarResolver const loaderProperties = { @@ -58,60 +60,53 @@ export class AppRunner { logLevel: params.loggingLevel as LogLevel, }; - // Create a resolver, that resolves componentsjs variables from cli-params - const varConfigFile = resolveAssetPath(params.varConfig); - this.createComponent( - loaderProperties as IComponentsManagerBuilderOptions, varConfigFile, {}, DEFAULT_VAR_RESOLVER, - ).then( - // Using varResolver resolve vars and start app - async(varResolver): Promise => { - let vars: VariableValues; - try { - vars = await varResolver.handle(argv); - } catch (error: unknown) { - throw new VError(error as Error, 'Error in computing variables'); - } - return this.startApp( - loaderProperties, resolveAssetPath(params.config), vars as unknown as Record, - ); - }, + const variableConfig = resolveAssetPath(params.varConfig); + this.resolveVariables(loaderProperties, variableConfig, argv) + .then((vars): Promise => this.startApp( + loaderProperties, resolveAssetPath(params.config), vars as unknown as Record, + )) + .catch((error): void => { + stderr.write(`Could not start the server\nCause:\n${error.message}\n`); + const stack = error instanceof VError ? error.cause()?.stack : error.stack; + stderr.write(`${stack}\n`); + process.exit(1); + }); + } - (error): void => { - throw new VError(error as Error, `Error in loading variable configuration from ${varConfigFile}`); - }, - ).catch((error): void => { - stderr.write(`Could not start the server\nCause:\n${error.message}\n`); - const stack = error instanceof VError ? error.cause()?.stack : error.stack; - stderr.write(`${stack}\n`); - process.exit(1); - }); + private async fulfillOrChain(promise: Promise, errorMessage: string): Promise { + let val: T; + try { + val = await promise; + } catch (error: unknown) { + throw new VError(error as Error, errorMessage); + } + return val; } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - private parseCliParams(argv: string[]) { - // eslint-disable-next-line no-sync - return setupBaseArgs(yargs(argv.slice(2)) - .usage('node ./bin/server.js [args]')).parseSync(); + private async resolveVariables( + loaderProperties: IComponentsManagerBuilderOptions, + configFile: string, argv: string[], + ): Promise { + // Create a resolver, that resolves componentsjs variables from cli-params + const resolver = await this.fulfillOrChain( + this.createComponent(loaderProperties, configFile, {}, DEFAULT_VAR_RESOLVER), + `Error in loading variable configuration from ${configFile}`, + ); + // Using varResolver resolve variables + return this.fulfillOrChain(resolver.handle(argv), 'Error in computing variables'); } private async startApp( loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, vars: Record, + configFile: string, vars: VariableValues, ): Promise { - let app: App; // Create app - try { - app = await this.createComponent(loaderProperties, configFile, vars, DEFAULT_APP); - } catch (error: unknown) { - throw new VError(error as Error, `Error: could not instantiate server from ${configFile}`); - } - + const app = await this.fulfillOrChain( + this.createComponent(loaderProperties, configFile, vars, DEFAULT_APP), + `Error: could not instantiate server from ${configFile}`, + ); // Execute app - try { - await app.start(); - } catch (error: unknown) { - throw new VError(error as Error, 'Could not start server'); - } + await this.fulfillOrChain(app.start(), 'Could not start server'); } public async createComponent( From 0f498242e5b2603f477ccee4ee69d364ff273a70 Mon Sep 17 00:00:00 2001 From: damooo Date: Mon, 6 Dec 2021 01:12:52 +0530 Subject: [PATCH 09/18] fix(AppRunner): verror dependency removal --- package-lock.json | 50 ------------------------------------------- package.json | 1 - src/init/AppRunner.ts | 9 ++++---- 3 files changed, 4 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff47a6d50e..11260f00ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,6 @@ "sparqljs": "^3.4.2", "url-join": "^4.0.1", "uuid": "^8.3.2", - "verror": "^1.10.1", "winston": "^3.3.3", "winston-transport": "^4.4.0", "ws": "^8.2.3", @@ -5484,14 +5483,6 @@ "node": ">=0.10.0" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "engines": { - "node": ">=0.8" - } - }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -8167,14 +8158,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ] - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -15320,19 +15303,6 @@ "node": ">= 0.8" } }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/vscode-textmate": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", @@ -20079,11 +20049,6 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -22103,11 +22068,6 @@ } } }, - "extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -27650,16 +27610,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, - "verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "vscode-textmate": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", diff --git a/package.json b/package.json index e4ca4e95e6..e7f96b2697 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,6 @@ "sparqljs": "^3.4.2", "url-join": "^4.0.1", "uuid": "^8.3.2", - "verror": "^1.10.1", "winston": "^3.3.3", "winston-transport": "^4.4.0", "ws": "^8.2.3", diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 064a135f04..e8674974ec 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -3,8 +3,8 @@ import type { ReadStream, WriteStream } from 'tty'; import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; -import VError from 'verror'; import yargs from 'yargs'; +import { createErrorMessage } from '..'; import { getLoggerFor } from '../logging/LogUtil'; import { resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; @@ -66,9 +66,8 @@ export class AppRunner { loaderProperties, resolveAssetPath(params.config), vars as unknown as Record, )) .catch((error): void => { - stderr.write(`Could not start the server\nCause:\n${error.message}\n`); - const stack = error instanceof VError ? error.cause()?.stack : error.stack; - stderr.write(`${stack}\n`); + stderr.write(`Could not start the server\nCause:\n${createErrorMessage(error)}\n`); + stderr.write(`${error.stack}\n`); process.exit(1); }); } @@ -78,7 +77,7 @@ export class AppRunner { try { val = await promise; } catch (error: unknown) { - throw new VError(error as Error, errorMessage); + throw new Error(`${errorMessage}\nCause: ${createErrorMessage(error)} `); } return val; } From 13f656c4b72e8b423c838b54b1d2898779f1615b Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 9 Dec 2021 17:08:43 +0100 Subject: [PATCH 10/18] fix: Simplify CLI error handling --- package-lock.json | 13 ------------- package.json | 1 - src/init/AppRunner.ts | 43 ++++++++++++++++--------------------------- 3 files changed, 16 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11260f00ab..14c2c49a95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,7 +78,6 @@ "@types/jest": "^27.0.0", "@types/set-cookie-parser": "^2.4.0", "@types/supertest": "^2.0.11", - "@types/verror": "^1.10.5", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", "cheerio": "^1.0.0-rc.10", @@ -4952,12 +4951,6 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, - "node_modules/@types/verror": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz", - "integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==", - "dev": true - }, "node_modules/@types/ws": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", @@ -19667,12 +19660,6 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" }, - "@types/verror": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz", - "integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==", - "dev": true - }, "@types/ws": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", diff --git a/package.json b/package.json index e7f96b2697..4580f267b2 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,6 @@ "@types/jest": "^27.0.0", "@types/set-cookie-parser": "^2.4.0", "@types/supertest": "^2.0.11", - "@types/verror": "^1.10.5", "@typescript-eslint/eslint-plugin": "^4.28.1", "@typescript-eslint/parser": "^4.28.1", "cheerio": "^1.0.0-rc.10", diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index e8674974ec..b695dcf280 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -1,5 +1,4 @@ /* eslint-disable unicorn/no-process-exit */ - import type { ReadStream, WriteStream } from 'tty'; import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; @@ -62,24 +61,20 @@ export class AppRunner { const variableConfig = resolveAssetPath(params.varConfig); this.resolveVariables(loaderProperties, variableConfig, argv) - .then((vars): Promise => this.startApp( - loaderProperties, resolveAssetPath(params.config), vars as unknown as Record, - )) - .catch((error): void => { - stderr.write(`Could not start the server\nCause:\n${createErrorMessage(error)}\n`); - stderr.write(`${error.stack}\n`); - process.exit(1); - }); + .then( + (vars): Promise => this.startApp(loaderProperties, resolveAssetPath(params.config), vars), + (error): void => this.writeError(stderr, 'Could not load config variables', error), + ) + .catch((error): void => this.writeError(stderr, 'Could not start the server', error)); } - private async fulfillOrChain(promise: Promise, errorMessage: string): Promise { - let val: T; - try { - val = await promise; - } catch (error: unknown) { - throw new Error(`${errorMessage}\nCause: ${createErrorMessage(error)} `); - } - return val; + /** + * Writes the given message and error to the `stderr` and exits the process. + */ + private writeError(stderr: WriteStream, message: string, error: Error): never { + stderr.write(`${message}\nCause:\n${createErrorMessage(error)}\n`); + stderr.write(`${error.stack}\n`); + process.exit(1); } private async resolveVariables( @@ -87,12 +82,9 @@ export class AppRunner { configFile: string, argv: string[], ): Promise { // Create a resolver, that resolves componentsjs variables from cli-params - const resolver = await this.fulfillOrChain( - this.createComponent(loaderProperties, configFile, {}, DEFAULT_VAR_RESOLVER), - `Error in loading variable configuration from ${configFile}`, - ); + const resolver = await this.createComponent(loaderProperties, configFile, {}, DEFAULT_VAR_RESOLVER); // Using varResolver resolve variables - return this.fulfillOrChain(resolver.handle(argv), 'Error in computing variables'); + return resolver.handle(argv); } private async startApp( @@ -100,12 +92,9 @@ export class AppRunner { configFile: string, vars: VariableValues, ): Promise { // Create app - const app = await this.fulfillOrChain( - this.createComponent(loaderProperties, configFile, vars, DEFAULT_APP), - `Error: could not instantiate server from ${configFile}`, - ); + const app = await this.createComponent(loaderProperties, configFile, vars, DEFAULT_APP); // Execute app - await this.fulfillOrChain(app.start(), 'Could not start server'); + await app.start(); } public async createComponent( From e5a7873927f566f051bec8aea175426a9eb1265b Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 10 Dec 2021 16:47:40 +0100 Subject: [PATCH 11/18] feat: Use same config for both CLI and app instantiation --- config/default.json | 2 ++ config/util/variables/default.json | 2 +- src/init/AppRunner.ts | 56 +++++++++++++++--------------- src/init/variables/VarResolver.ts | 2 -- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/config/default.json b/config/default.json index aa4346295b..4d6799a18d 100644 --- a/config/default.json +++ b/config/default.json @@ -1,6 +1,8 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld", "import": [ + "files-scs:config/app-setup/vars.json", + "files-scs:config/app/main/default.json", "files-scs:config/app/init/initialize-prefilled-root.json", "files-scs:config/app/setup/optional.json", diff --git a/config/util/variables/default.json b/config/util/variables/default.json index d259bcfb6f..beb5aabb36 100644 --- a/config/util/variables/default.json +++ b/config/util/variables/default.json @@ -8,7 +8,7 @@ "@type": "Variable" }, { - "comment": "Needs to be set to the base URL of the server for authnetication and authorization to function.", + "comment": "Needs to be set to the base URL of the server for authentication and authorization to function.", "@id": "urn:solid-server:default:variable:baseUrl", "@type": "Variable" }, diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index b695dcf280..3fa460a978 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -28,8 +28,9 @@ export class AppRunner { configFile: string, variables: VariableValues, ): Promise { - const app = await this.createComponent(loaderProperties, configFile, variables, DEFAULT_APP); - await app.start(); + const componentsManager = await this.createComponentsManager(loaderProperties, configFile); + const defaultVariables = await this.resolveVariables(componentsManager, []); + await this.startApp(componentsManager, { ...defaultVariables, ...variables }); } /** @@ -59,13 +60,17 @@ export class AppRunner { logLevel: params.loggingLevel as LogLevel, }; - const variableConfig = resolveAssetPath(params.varConfig); - this.resolveVariables(loaderProperties, variableConfig, argv) - .then( - (vars): Promise => this.startApp(loaderProperties, resolveAssetPath(params.config), vars), - (error): void => this.writeError(stderr, 'Could not load config variables', error), - ) - .catch((error): void => this.writeError(stderr, 'Could not start the server', error)); + const config = resolveAssetPath(params.config); + this.createComponentsManager(loaderProperties, config) + .then((componentsManager): Promise => + this.resolveVariables(componentsManager, argv).then( + (vars): Promise => this.startApp(componentsManager, vars), + (error): void => this.writeError(stderr, `Could not load config variables from ${config}`, error), + ).catch((error): void => { + this.logger.error(`Could not start server: ${error}`); + process.exit(1); + })) + .catch((error): void => this.writeError(stderr, `Could not build the config files from ${config}`, error)); } /** @@ -77,37 +82,32 @@ export class AppRunner { process.exit(1); } - private async resolveVariables( - loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, argv: string[], - ): Promise { + private async resolveVariables(componentsManager: ComponentsManager, argv: string[]): + Promise { // Create a resolver, that resolves componentsjs variables from cli-params - const resolver = await this.createComponent(loaderProperties, configFile, {}, DEFAULT_VAR_RESOLVER); + const resolver = await componentsManager.instantiate(DEFAULT_VAR_RESOLVER, {}); // Using varResolver resolve variables - return resolver.handle(argv); + return resolver.handleSafe(argv); } - private async startApp( - loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, vars: VariableValues, - ): Promise { + private async startApp(componentsManager: ComponentsManager, variables: VariableValues): Promise { // Create app - const app = await this.createComponent(loaderProperties, configFile, vars, DEFAULT_APP); + const app = await componentsManager.instantiate(DEFAULT_APP, { variables }); // Execute app await app.start(); } - public async createComponent( - loaderProperties: IComponentsManagerBuilderOptions, + /** + * Creates the Components Manager that will be used for instantiating. + * Typing is set to `any` since it will be used for instantiating multiple objects. + */ + public async createComponentsManager( + loaderProperties: IComponentsManagerBuilderOptions, configFile: string, - variables: Record, - componentIri: string, - ): Promise { + ): Promise> { // Set up Components.js const componentsManager = await ComponentsManager.build(loaderProperties); await componentsManager.configRegistry.register(configFile); - - // Create the component - return await componentsManager.instantiate(componentIri, { variables }); + return componentsManager; } } diff --git a/src/init/variables/VarResolver.ts b/src/init/variables/VarResolver.ts index cc78238ec5..86feccd13d 100644 --- a/src/init/variables/VarResolver.ts +++ b/src/init/variables/VarResolver.ts @@ -6,7 +6,6 @@ import { modulePathPlaceholder } from '../../util/PathUtil'; import type { VarComputer } from './VarComputer'; const defaultConfig = `${modulePathPlaceholder}config/default.json`; -const defaultVarConfig = `${modulePathPlaceholder}config/app-setup/vars.json`; export type YargsArgOptions = Record; @@ -20,7 +19,6 @@ export function setupBaseArgs(yargv: yargs.Argv) { config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS }, mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, - varConfig: { type: 'string', alias: 'v', default: defaultVarConfig, requiresArg: true }, }); } From b13515845253dff10cdc66cddbd3dd77f4d1f7f7 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 13 Dec 2021 09:59:35 +0100 Subject: [PATCH 12/18] fix: Update typings and imports --- src/index.ts | 16 ++++--- src/init/AppRunner.ts | 2 +- src/init/variables/VarComputer.ts | 3 +- src/init/variables/VarResolver.ts | 44 +++++++++---------- src/init/variables/computers/ArgExtractor.ts | 3 +- .../variables/computers/AssetPathResolver.ts | 5 +-- .../variables/computers/BaseUrlComputer.ts | 11 +++++ .../variables/computers/BaseUrlResolver.ts | 12 ----- 8 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 src/init/variables/computers/BaseUrlComputer.ts delete mode 100644 src/init/variables/computers/BaseUrlResolver.ts diff --git a/src/index.ts b/src/index.ts index ce1eb8198d..0e313a6582 100644 --- a/src/index.ts +++ b/src/index.ts @@ -177,6 +177,15 @@ export * from './init/final/ParallelFinalizer'; // Init/Setup export * from './init/setup/SetupHttpHandler'; +// Init/Variables/Computers +export * from './init/variables/computers/ArgExtractor'; +export * from './init/variables/computers/AssetPathResolver'; +export * from './init/variables/computers/BaseUrlComputer'; + +// Init/Variables +export * from './init/variables/VarResolver'; +export * from './init/variables/VarComputer'; + // Init export * from './init/App'; export * from './init/AppRunner'; @@ -186,13 +195,6 @@ export * from './init/Initializer'; export * from './init/LoggerInitializer'; export * from './init/ServerInitializer'; -// Init/Vars -export * from './init/variables/VarResolver'; -export * from './init/variables/VarComputer'; -export * from './init/variables/computers/ArgExtractor'; -export * from './init/variables/computers/AssetPathResolver'; -export * from './init/variables/computers/BaseUrlResolver'; - // Logging export * from './logging/LazyLogger'; export * from './logging/LazyLoggerFactory'; diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 3fa460a978..baa04103da 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -3,8 +3,8 @@ import type { ReadStream, WriteStream } from 'tty'; import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; import yargs from 'yargs'; -import { createErrorMessage } from '..'; import { getLoggerFor } from '../logging/LogUtil'; +import { createErrorMessage } from '../util/errors/ErrorUtil'; import { resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; import type { VarResolver, VariableValues } from './variables/VarResolver'; diff --git a/src/init/variables/VarComputer.ts b/src/init/variables/VarComputer.ts index 2877ec2a0e..b6c16820b7 100644 --- a/src/init/variables/VarComputer.ts +++ b/src/init/variables/VarComputer.ts @@ -1,8 +1,7 @@ -import type yargs from 'yargs'; import { AsyncHandler } from '../../util/handlers/AsyncHandler'; /** A handler that determines the value of a specific variable from parsed cli arguments. */ -export abstract class VarComputer extends AsyncHandler { +export abstract class VarComputer extends AsyncHandler, unknown> { } diff --git a/src/init/variables/VarResolver.ts b/src/init/variables/VarResolver.ts index 86feccd13d..72a45286c8 100644 --- a/src/init/variables/VarResolver.ts +++ b/src/init/variables/VarResolver.ts @@ -1,27 +1,29 @@ /* eslint-disable tsdoc/syntax */ +import type { Arguments, Argv, InferredOptionTypes, Options } from 'yargs'; import yargs from 'yargs'; -import { createErrorMessage, LOG_LEVELS } from '../..'; +import { LOG_LEVELS } from '../../logging/LogLevel'; +import { createErrorMessage } from '../../util/errors/ErrorUtil'; import { AsyncHandler } from '../../util/handlers/AsyncHandler'; import { modulePathPlaceholder } from '../../util/PathUtil'; import type { VarComputer } from './VarComputer'; const defaultConfig = `${modulePathPlaceholder}config/default.json`; +/** + * CLI options needed for performing meta-process of app initialization. + * These options doesn't contribute to components-js vars normally. + */ +const baseArgs = { + config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, + loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS }, + mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, +} as const; -export type YargsArgOptions = Record; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function setupBaseArgs(yargv: yargs.Argv) { - return yargv.options({ - /** - * CLI options needed for performing meta-process of app initialization. - * These options doesn't contribute to components-js vars normally. - */ - config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, - loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS }, - mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, - }); +export function setupBaseArgs(yArgv: Argv): Argv> { + return yArgv.options(baseArgs); } +export type YargsArgOptions = Record; + export interface CliOptions { // Usage string to be given at cli usage?: string; @@ -49,25 +51,23 @@ export class VarResolver extends AsyncHandler { /** * @param parameters - record of option to it's yargs opt config. @range {json} * @param options - options to configure yargv. @range {json} - * @param varComputers - record of componentsjs var-iri to VarComputer. + * @param varComputers - record of componentsjs var-iri to VarComputer. */ - public constructor( - parameters: YargsArgOptions, options: CliOptions, varComputers: Record, - ) { + public constructor(parameters: YargsArgOptions, options: CliOptions, varComputers: Record) { super(); this.yargsArgOptions = parameters; this.yargvOptions = options; this.varComputers = varComputers; } - private async parseArgs(argv: readonly string[]): Promise { + private async parseArgs(argv: readonly string[]): Promise { let yArgv = this.createYArgv(argv); yArgv = this.validateArguments(yArgv); return yArgv.parse(); } - private createYArgv(argv: readonly string[]): yargs.Argv { + private createYArgv(argv: readonly string[]): Argv { let yArgv = yargs(argv.slice(2)); if (this.yargvOptions.usage !== undefined) { yArgv = yArgv.usage(this.yargvOptions.usage); @@ -81,7 +81,7 @@ export class VarResolver extends AsyncHandler { return setupBaseArgs(yArgv.options(this.yargsArgOptions)); } - private validateArguments(yArgv: yargs.Argv): yargs.Argv { + private validateArguments(yArgv: Argv): Argv { return yArgv.check((args): boolean => { if (args._.length > 0) { throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); @@ -96,7 +96,7 @@ export class VarResolver extends AsyncHandler { }); } - private async resolveVariables(args: yargs.Arguments): Promise> { + private async resolveVariables(args: Record): Promise> { const vars: Record = {}; for (const [ name, computer ] of Object.entries(this.varComputers)) { try { diff --git a/src/init/variables/computers/ArgExtractor.ts b/src/init/variables/computers/ArgExtractor.ts index 50d852e7ff..ab10399860 100644 --- a/src/init/variables/computers/ArgExtractor.ts +++ b/src/init/variables/computers/ArgExtractor.ts @@ -1,4 +1,3 @@ -import type yargs from 'yargs'; import { VarComputer } from '../VarComputer'; /** @@ -12,7 +11,7 @@ export class ArgExtractor extends VarComputer { this.argKey = argKey; } - public async handle(args: yargs.Arguments): Promise { + public async handle(args: Record): Promise { return args[this.argKey]; } } diff --git a/src/init/variables/computers/AssetPathResolver.ts b/src/init/variables/computers/AssetPathResolver.ts index f86a9be9d9..8e1ea5cf45 100644 --- a/src/init/variables/computers/AssetPathResolver.ts +++ b/src/init/variables/computers/AssetPathResolver.ts @@ -1,5 +1,4 @@ -import type yargs from 'yargs'; -import { resolveAssetPath } from '../../..'; +import { resolveAssetPath } from '../../../util/PathUtil'; import { VarComputer } from '../VarComputer'; /** @@ -14,7 +13,7 @@ export class AssetPathResolver extends VarComputer { this.pathArgKey = pathArgKey; } - public async handle(args: yargs.Arguments): Promise { + public async handle(args: Record): Promise { const path = args[this.pathArgKey]; if (typeof path !== 'string') { throw new Error(`Invalid ${this.pathArgKey} argument`); diff --git a/src/init/variables/computers/BaseUrlComputer.ts b/src/init/variables/computers/BaseUrlComputer.ts new file mode 100644 index 0000000000..1221c8e802 --- /dev/null +++ b/src/init/variables/computers/BaseUrlComputer.ts @@ -0,0 +1,11 @@ +import { ensureTrailingSlash } from '../../../util/PathUtil'; +import { VarComputer } from '../VarComputer'; + +/** + * A handler to compute base-url from args + */ +export class BaseUrlComputer extends VarComputer { + public async handle(args: Record): Promise { + return typeof args.baseUrl === 'string' ? ensureTrailingSlash(args.baseUrl) : `http://localhost:${args.port}/`; + } +} diff --git a/src/init/variables/computers/BaseUrlResolver.ts b/src/init/variables/computers/BaseUrlResolver.ts deleted file mode 100644 index 615b779fcc..0000000000 --- a/src/init/variables/computers/BaseUrlResolver.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type yargs from 'yargs'; -import { ensureTrailingSlash } from '../../..'; -import { VarComputer } from '../VarComputer'; - -/** - * A handler to compute base-url from args - */ -export class BaseUrlComputer extends VarComputer { - public async handle(args: yargs.Arguments): Promise { - return args.baseUrl ? ensureTrailingSlash(args.baseUrl as string) : `http://localhost:${args.port}/`; - } -} From 8d6470fd6eedde7c23e629eac836cfbc57f27117 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 14 Dec 2021 10:54:30 +0100 Subject: [PATCH 13/18] feat: Split VariableResolver behaviour to 2 classes --- config/app-setup/vars.json | 154 ++++++++---------- src/index.ts | 9 +- src/init/AppRunner.ts | 58 +++++-- src/init/cli/CliExtractor.ts | 14 ++ src/init/cli/YargsCliExtractor.ts | 70 ++++++++ src/init/variables/ComputerResolver.ts | 27 +++ src/init/variables/VarComputer.ts | 7 - src/init/variables/VarResolver.ts | 115 ------------- src/init/variables/VariableResolver.ts | 8 + src/init/variables/computers/ArgExtractor.ts | 4 +- .../variables/computers/AssetPathResolver.ts | 4 +- .../variables/computers/BaseUrlComputer.ts | 4 +- src/init/variables/computers/ValueComputer.ts | 6 + 13 files changed, 250 insertions(+), 230 deletions(-) create mode 100644 src/init/cli/CliExtractor.ts create mode 100644 src/init/cli/YargsCliExtractor.ts create mode 100644 src/init/variables/ComputerResolver.ts delete mode 100644 src/init/variables/VarComputer.ts delete mode 100644 src/init/variables/VarResolver.ts create mode 100644 src/init/variables/VariableResolver.ts create mode 100644 src/init/variables/computers/ValueComputer.ts diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json index 3311dc5d45..4f2ffc63d3 100644 --- a/config/app-setup/vars.json +++ b/config/app-setup/vars.json @@ -5,93 +5,81 @@ "@graph": [ { "comment": "VarResolver resolves variables to be used in app instantiation, from cli args", - "@id": "urn:solid-server-app-setup:default:VarResolver", - "@type": "VarResolver", - "parameters": { - "baseUrl": { - "alias": "b", - "requiresArg": true, - "type": "string" - }, - "port": { - "alias": "p", - "requiresArg": true, - "type": "number", - "default": 3000 - }, - "rootFilePath": { - "alias": "f", - "requiresArg": true, - "type": "string", - "default": "./" - }, - "showStackTrace": { - "alias": "t", - "type": "boolean", - "default": false - }, - "sparqlEndpoint": { - "alias": "s", - "requiresArg": true, - "type": "string" - }, - "podConfigJson": { - "requiresArg": true, - "type": "string", - "default": "./pod-config.json" - } - }, - "options": { - "usage": "node ./bin/server.js [args]", - "strictMode": true - }, - "varComputers": [ - { - "VarResolver:_varComputers_key": "urn:solid-server:default:variable:baseUrl", - "VarResolver:_varComputers_value": { - "@type": "BaseUrlComputer" - } - }, - { - "VarResolver:_varComputers_key": "urn:solid-server:default:variable:loggingLevel", - "VarResolver:_varComputers_value": { - "@type": "ArgExtractor", - "argKey": "loggingLevel" - } - }, - { - "VarResolver:_varComputers_key": "urn:solid-server:default:variable:port", - "VarResolver:_varComputers_value": { - "@type": "ArgExtractor", - "argKey": "port" - } - }, - { - "VarResolver:_varComputers_key": "urn:solid-server:default:variable:rootFilePath", - "VarResolver:_varComputers_value": { - "@type": "AssetPathResolver", - "pathArgKey": "rootFilePath" - } - }, - { - "VarResolver:_varComputers_key": "urn:solid-server:default:variable:sparqlEndpoint", - "VarResolver:_varComputers_value": { - "@type": "ArgExtractor", - "argKey": "sparqlEndpoint" - } - }, + "@id": "urn:solid-server-app-setup:default:CliResolver", + "@type": "RecordObject", + "record": [ { - "VarResolver:_varComputers_key": "urn:solid-server:default:variable:showStackTrace", - "VarResolver:_varComputers_value": { - "@type": "ArgExtractor", - "argKey": "showStackTrace" + "RecordObject:_record_key": "extractor", + "RecordObject:_record_value": { + "@type": "YargsCliExtractor", + "parameters": { + "loggingLevel": { "alias": "l", "requiresArg": true, "type": "string", "default": "info" }, + "baseUrl": { "alias": "b", "requiresArg": true, "type": "string" }, + "port": { "alias": "p", "requiresArg": true, "type": "number", "default": 3000 }, + "rootFilePath": { "alias": "f", "requiresArg": true, "type": "string", "default": "./" }, + "showStackTrace": { "alias": "t", "type": "boolean", "default": false }, + "sparqlEndpoint": { "alias": "s", "requiresArg": true, "type": "string" }, + "podConfigJson": { "requiresArg": true, "type": "string", "default": "./pod-config.json" } + }, + "options": { + "usage": "node ./bin/server.js [args]" + } } }, { - "VarResolver:_varComputers_key": "urn:solid-server:default:variable:AssetPathResolver", - "VarResolver:_varComputers_value": { - "@type": "AssetPathResolver", - "pathArgKey": "podConfigJson" + "RecordObject:_record_key": "resolver", + "RecordObject:_record_value": { + "@type": "ComputerResolver", + "computers": [ + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:baseUrl", + "ComputerResolver:_computers_value": { + "@type": "BaseUrlComputer" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:loggingLevel", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "argKey": "loggingLevel" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:port", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "argKey": "port" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:rootFilePath", + "ComputerResolver:_computers_value": { + "@type": "AssetPathResolver", + "pathArgKey": "rootFilePath" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:sparqlEndpoint", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "argKey": "sparqlEndpoint" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:showStackTrace", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "argKey": "showStackTrace" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:AssetPathResolver", + "ComputerResolver:_computers_value": { + "@type": "AssetPathResolver", + "pathArgKey": "podConfigJson" + } + } + ] } } ] diff --git a/src/index.ts b/src/index.ts index 0e313a6582..7c7039eaf0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -177,14 +177,19 @@ export * from './init/final/ParallelFinalizer'; // Init/Setup export * from './init/setup/SetupHttpHandler'; +// Init/Cli +export * from './init/cli/CliExtractor'; +export * from './init/cli/YargsCliExtractor'; + // Init/Variables/Computers export * from './init/variables/computers/ArgExtractor'; export * from './init/variables/computers/AssetPathResolver'; export * from './init/variables/computers/BaseUrlComputer'; +export * from './init/variables/computers/ValueComputer'; // Init/Variables -export * from './init/variables/VarResolver'; -export * from './init/variables/VarComputer'; +export * from './init/variables/ComputerResolver'; +export * from './init/variables/VariableResolver'; // Init export * from './init/App'; diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index baa04103da..40cfbe815a 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -1,18 +1,34 @@ /* eslint-disable unicorn/no-process-exit */ import type { ReadStream, WriteStream } from 'tty'; -import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; +import type { IComponentsManagerBuilderOptions } from 'componentsjs'; import { ComponentsManager } from 'componentsjs'; import yargs from 'yargs'; +import type { LogLevel } from '../logging/LogLevel'; +import { LOG_LEVELS } from '../logging/LogLevel'; import { getLoggerFor } from '../logging/LogUtil'; import { createErrorMessage } from '../util/errors/ErrorUtil'; -import { resolveAssetPath } from '../util/PathUtil'; +import { modulePathPlaceholder, resolveAssetPath } from '../util/PathUtil'; import type { App } from './App'; -import type { VarResolver, VariableValues } from './variables/VarResolver'; -import { setupBaseArgs } from './variables/VarResolver'; +import type { CliExtractor } from './cli/CliExtractor'; +import type { VariableResolver } from './variables/VariableResolver'; -const DEFAULT_VAR_RESOLVER = 'urn:solid-server-app-setup:default:VarResolver'; +const defaultConfig = `${modulePathPlaceholder}config/default.json`; + +const DEFAULT_CLI_RESOLVER = 'urn:solid-server-app-setup:default:CliResolver'; const DEFAULT_APP = 'urn:solid-server:default:App'; +const baseArgs = { + config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, + loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS }, + mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, +} as const; + +// The components needed by the AppRunner to start up the server +interface CliResolver { + extractor?: CliExtractor; + resolver: VariableResolver; +} + export class AppRunner { private readonly logger = getLoggerFor(this); @@ -21,16 +37,17 @@ export class AppRunner { * This method can be used to start the server from within another JavaScript application. * @param loaderProperties - Components.js loader properties. * @param configFile - Path to the server config file. - * @param variables - Variables to pass into the config file. + * @param parameters - Parameters to pass into the VariableResolver. */ public async run( loaderProperties: IComponentsManagerBuilderOptions, configFile: string, - variables: VariableValues, + parameters: Record, ): Promise { const componentsManager = await this.createComponentsManager(loaderProperties, configFile); - const defaultVariables = await this.resolveVariables(componentsManager, []); - await this.startApp(componentsManager, { ...defaultVariables, ...variables }); + const resolver = await componentsManager.instantiate(DEFAULT_CLI_RESOLVER, {}); + const variables = await resolver.resolver.handleSafe(parameters); + await this.startApp(componentsManager, variables); } /** @@ -48,10 +65,12 @@ export class AppRunner { stdout?: WriteStream; stderr?: WriteStream; } = {}): void { - // Parse the command-line arguments + // Minimal parsing of the CLI parameters, so we can create a CliResolver // eslint-disable-next-line no-sync - const params = setupBaseArgs(yargs(argv.slice(2)) - .usage('node ./bin/server.js [args]')).parseSync(); + const params = yargs(argv.slice(2)) + .usage('node ./bin/server.js [args]') + .options(baseArgs) + .parseSync(); // Gather settings for instantiating VarResolver const loaderProperties = { @@ -82,15 +101,20 @@ export class AppRunner { process.exit(1); } - private async resolveVariables(componentsManager: ComponentsManager, argv: string[]): - Promise { + private async resolveVariables(componentsManager: ComponentsManager, argv: string[]): + Promise> { // Create a resolver, that resolves componentsjs variables from cli-params - const resolver = await componentsManager.instantiate(DEFAULT_VAR_RESOLVER, {}); + const resolver = await componentsManager.instantiate(DEFAULT_CLI_RESOLVER, {}); + // Extract values from CLI parameters + if (!resolver.extractor) { + throw new Error('No CliExtractor is defined.'); + } + const cliValues = await resolver.extractor.handleSafe(argv); // Using varResolver resolve variables - return resolver.handleSafe(argv); + return await resolver.resolver.handleSafe(cliValues); } - private async startApp(componentsManager: ComponentsManager, variables: VariableValues): Promise { + private async startApp(componentsManager: ComponentsManager, variables: Record): Promise { // Create app const app = await componentsManager.instantiate(DEFAULT_APP, { variables }); // Execute app diff --git a/src/init/cli/CliExtractor.ts b/src/init/cli/CliExtractor.ts new file mode 100644 index 0000000000..ca910ceb4d --- /dev/null +++ b/src/init/cli/CliExtractor.ts @@ -0,0 +1,14 @@ +import { AsyncHandler } from '../../util/handlers/AsyncHandler'; + +/** + * Converts the input CLI arguments into an easily parseable key/value object. + * + * There are certain CLI parameters that are required before this class can be instantiated. + * These can be ignored by this class, but that does mean that this class should not error if they are present. + * + * Those CLI parameters are specifically: + * - -c / \--config + * - -m / \--mainModulePath + * - -l / \--loggingLevel + */ +export abstract class CliExtractor extends AsyncHandler> {} diff --git a/src/init/cli/YargsCliExtractor.ts b/src/init/cli/YargsCliExtractor.ts new file mode 100644 index 0000000000..228500ae4d --- /dev/null +++ b/src/init/cli/YargsCliExtractor.ts @@ -0,0 +1,70 @@ +/* eslint-disable tsdoc/syntax */ +import type { Arguments, Argv, Options } from 'yargs'; +import yargs from 'yargs'; +import { CliExtractor } from './CliExtractor'; + +export type YargsArgOptions = Record; + +export interface CliOptions { + // Usage string to be given at cli + usage?: string; + // StrictMode determines wether to allow undefined cli-args or not. + strictMode?: boolean; + // Wether to load arguments from env-vars or not. + // @see http://yargs.js.org/docs/#api-reference-envprefix + loadFromEnv?: boolean; + // Prefix for env-vars. + // see yargv docs for behavior. http://yargs.js.org/docs/#api-reference-envprefix + envVarPrefix?: string; +} + +export class YargsCliExtractor extends CliExtractor { + protected readonly yargsArgOptions: YargsArgOptions; + protected readonly yargvOptions: CliOptions; + + /** + * @param parameters - record of option to it's yargs opt config. @range {json} + * @param options - options to configure yargv. @range {json} + */ + public constructor(parameters: YargsArgOptions = {}, options: CliOptions = {}) { + super(); + this.yargsArgOptions = parameters; + this.yargvOptions = options; + } + + public async handle(argv: readonly string[]): Promise { + let yArgv = this.createYArgv(argv); + yArgv = this.validateArguments(yArgv); + + return yArgv.parse(); + } + + private createYArgv(argv: readonly string[]): Argv { + let yArgv = yargs(argv.slice(2)); + if (this.yargvOptions.usage !== undefined) { + yArgv = yArgv.usage(this.yargvOptions.usage); + } + if (this.yargvOptions.strictMode) { + yArgv = yArgv.strict(); + } + if (this.yargvOptions.loadFromEnv) { + yArgv = yArgv.env(this.yargvOptions.envVarPrefix ?? ''); + } + return yArgv.options(this.yargsArgOptions); + } + + private validateArguments(yArgv: Argv): Argv { + return yArgv.check((args): boolean => { + if (args._.length > 0) { + throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); + } + for (const [ key, val ] of Object.entries(args)) { + // We have no options that allow for arrays + if (key !== '_' && Array.isArray(val)) { + throw new Error(`Multiple values were provided for: "${key}": "${val.join('", "')}"`); + } + } + return true; + }); + } +} diff --git a/src/init/variables/ComputerResolver.ts b/src/init/variables/ComputerResolver.ts new file mode 100644 index 0000000000..0d0413e975 --- /dev/null +++ b/src/init/variables/ComputerResolver.ts @@ -0,0 +1,27 @@ +import { createErrorMessage } from '../../util/errors/ErrorUtil'; +import type { ValueComputer } from './computers/ValueComputer'; +import { VariableResolver } from './VariableResolver'; + +/** + * Generates variable values by running a set of {@link ValueComputer}s on the input. + */ +export class ComputerResolver extends VariableResolver { + public readonly computers: Record; + + public constructor(computers: Record) { + super(); + this.computers = computers; + } + + public async handle(input: Record): Promise> { + const vars: Record = {}; + for (const [ name, computer ] of Object.entries(this.computers)) { + try { + vars[name] = await computer.handle(input); + } catch (err: unknown) { + throw new Error(`Error in computing value for variable ${name}: ${createErrorMessage(err)}`); + } + } + return vars; + } +} diff --git a/src/init/variables/VarComputer.ts b/src/init/variables/VarComputer.ts deleted file mode 100644 index b6c16820b7..0000000000 --- a/src/init/variables/VarComputer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AsyncHandler } from '../../util/handlers/AsyncHandler'; - -/** - A handler that determines the value of a specific variable from parsed cli arguments. - */ -export abstract class VarComputer extends AsyncHandler, unknown> { -} diff --git a/src/init/variables/VarResolver.ts b/src/init/variables/VarResolver.ts deleted file mode 100644 index 72a45286c8..0000000000 --- a/src/init/variables/VarResolver.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable tsdoc/syntax */ -import type { Arguments, Argv, InferredOptionTypes, Options } from 'yargs'; -import yargs from 'yargs'; -import { LOG_LEVELS } from '../../logging/LogLevel'; -import { createErrorMessage } from '../../util/errors/ErrorUtil'; -import { AsyncHandler } from '../../util/handlers/AsyncHandler'; -import { modulePathPlaceholder } from '../../util/PathUtil'; -import type { VarComputer } from './VarComputer'; - -const defaultConfig = `${modulePathPlaceholder}config/default.json`; -/** - * CLI options needed for performing meta-process of app initialization. - * These options doesn't contribute to components-js vars normally. - */ -const baseArgs = { - config: { type: 'string', alias: 'c', default: defaultConfig, requiresArg: true }, - loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS }, - mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, -} as const; - -export function setupBaseArgs(yArgv: Argv): Argv> { - return yArgv.options(baseArgs); -} - -export type YargsArgOptions = Record; - -export interface CliOptions { - // Usage string to be given at cli - usage?: string; - // StrictMode determines wether to allow undefined cli-args or not. - strictMode?: boolean; - // Wether to load arguments from env-vars or not. - // @see http://yargs.js.org/docs/#api-reference-envprefix - loadFromEnv?: boolean; - // Prefix for env-vars. - // see yargv docs for behavior. http://yargs.js.org/docs/#api-reference-envprefix - envVarPrefix?: string; -} - -export type VariableValues = Record; - -/** - * This class translates command-line arguments/env-vars into values for specific variables, - * which can then be used to instantiate a parametrized Components.js configuration. - */ -export class VarResolver extends AsyncHandler { - protected readonly yargsArgOptions: YargsArgOptions; - protected readonly yargvOptions: CliOptions; - protected readonly varComputers: Record; - - /** - * @param parameters - record of option to it's yargs opt config. @range {json} - * @param options - options to configure yargv. @range {json} - * @param varComputers - record of componentsjs var-iri to VarComputer. - */ - public constructor(parameters: YargsArgOptions, options: CliOptions, varComputers: Record) { - super(); - this.yargsArgOptions = parameters; - this.yargvOptions = options; - this.varComputers = varComputers; - } - - private async parseArgs(argv: readonly string[]): Promise { - let yArgv = this.createYArgv(argv); - yArgv = this.validateArguments(yArgv); - - return yArgv.parse(); - } - - private createYArgv(argv: readonly string[]): Argv { - let yArgv = yargs(argv.slice(2)); - if (this.yargvOptions.usage !== undefined) { - yArgv = yArgv.usage(this.yargvOptions.usage); - } - if (this.yargvOptions.strictMode) { - yArgv = yArgv.strict(); - } - if (this.yargvOptions.loadFromEnv) { - yArgv = yArgv.env(this.yargvOptions.envVarPrefix ?? ''); - } - return setupBaseArgs(yArgv.options(this.yargsArgOptions)); - } - - private validateArguments(yArgv: Argv): Argv { - return yArgv.check((args): boolean => { - if (args._.length > 0) { - throw new Error(`Unsupported positional arguments: "${args._.join('", "')}"`); - } - for (const [ key, val ] of Object.entries(args)) { - // We have no options that allow for arrays - if (key !== '_' && Array.isArray(val)) { - throw new Error(`Multiple values were provided for: "${key}": "${val.join('", "')}"`); - } - } - return true; - }); - } - - private async resolveVariables(args: Record): Promise> { - const vars: Record = {}; - for (const [ name, computer ] of Object.entries(this.varComputers)) { - try { - vars[name] = await computer.handle(args); - } catch (err: unknown) { - throw new Error(`Error in computing value for variable ${name}: ${createErrorMessage(err)}`); - } - } - return vars; - } - - public async handle(argv: readonly string[]): Promise { - const args = await this.parseArgs(argv); - return this.resolveVariables(args); - } -} diff --git a/src/init/variables/VariableResolver.ts b/src/init/variables/VariableResolver.ts new file mode 100644 index 0000000000..8210031080 --- /dev/null +++ b/src/init/variables/VariableResolver.ts @@ -0,0 +1,8 @@ +import { AsyncHandler } from '../../util/handlers/AsyncHandler'; + +/** + * Converts a key/value object, extracted from the CLI or passed as a parameter, + * into a new key/value object where the keys are variables defined in the Components.js configuration. + * The resulting values are the values that should be assigned to those variables. + */ +export abstract class VariableResolver extends AsyncHandler, Record> {} diff --git a/src/init/variables/computers/ArgExtractor.ts b/src/init/variables/computers/ArgExtractor.ts index ab10399860..d880d1d3f3 100644 --- a/src/init/variables/computers/ArgExtractor.ts +++ b/src/init/variables/computers/ArgExtractor.ts @@ -1,9 +1,9 @@ -import { VarComputer } from '../VarComputer'; +import { ValueComputer } from './ValueComputer'; /** * Simple VarComputer that just extracts an arg from parsed args. */ -export class ArgExtractor extends VarComputer { +export class ArgExtractor extends ValueComputer { private readonly argKey: string; public constructor(argKey: string) { diff --git a/src/init/variables/computers/AssetPathResolver.ts b/src/init/variables/computers/AssetPathResolver.ts index 8e1ea5cf45..6092c0683d 100644 --- a/src/init/variables/computers/AssetPathResolver.ts +++ b/src/init/variables/computers/AssetPathResolver.ts @@ -1,11 +1,11 @@ import { resolveAssetPath } from '../../../util/PathUtil'; -import { VarComputer } from '../VarComputer'; +import { ValueComputer } from './ValueComputer'; /** * This `VarComputer` resolves absolute path of asset, from path specified in specified argument. * It follows conventions of `resolveAssetPath` function for path resolution. */ -export class AssetPathResolver extends VarComputer { +export class AssetPathResolver extends ValueComputer { private readonly pathArgKey: string; public constructor(pathArgKey: string) { diff --git a/src/init/variables/computers/BaseUrlComputer.ts b/src/init/variables/computers/BaseUrlComputer.ts index 1221c8e802..1572b54596 100644 --- a/src/init/variables/computers/BaseUrlComputer.ts +++ b/src/init/variables/computers/BaseUrlComputer.ts @@ -1,10 +1,10 @@ import { ensureTrailingSlash } from '../../../util/PathUtil'; -import { VarComputer } from '../VarComputer'; +import { ValueComputer } from './ValueComputer'; /** * A handler to compute base-url from args */ -export class BaseUrlComputer extends VarComputer { +export class BaseUrlComputer extends ValueComputer { public async handle(args: Record): Promise { return typeof args.baseUrl === 'string' ? ensureTrailingSlash(args.baseUrl) : `http://localhost:${args.port}/`; } diff --git a/src/init/variables/computers/ValueComputer.ts b/src/init/variables/computers/ValueComputer.ts new file mode 100644 index 0000000000..8c81657e2c --- /dev/null +++ b/src/init/variables/computers/ValueComputer.ts @@ -0,0 +1,6 @@ +import { AsyncHandler } from '../../../util/handlers/AsyncHandler'; + +/** + A handler that determines the value of a specific variable from parsed cli arguments. + */ +export abstract class ValueComputer extends AsyncHandler, unknown> {} From a6692d8cf428c6cc00a1b9f658a6265b54d02923 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 14 Dec 2021 11:14:18 +0100 Subject: [PATCH 14/18] feat: Move default value behaviour from CLI to ValueComputers --- config/app-setup/vars.json | 27 +++++++++++-------- src/init/variables/computers/ArgExtractor.ts | 10 ++++--- .../variables/computers/AssetPathResolver.ts | 12 +++++---- .../variables/computers/BaseUrlComputer.ts | 13 ++++++++- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json index 4f2ffc63d3..7bbb7a04a6 100644 --- a/config/app-setup/vars.json +++ b/config/app-setup/vars.json @@ -13,13 +13,13 @@ "RecordObject:_record_value": { "@type": "YargsCliExtractor", "parameters": { - "loggingLevel": { "alias": "l", "requiresArg": true, "type": "string", "default": "info" }, + "loggingLevel": { "alias": "l", "requiresArg": true, "type": "string" }, "baseUrl": { "alias": "b", "requiresArg": true, "type": "string" }, - "port": { "alias": "p", "requiresArg": true, "type": "number", "default": 3000 }, - "rootFilePath": { "alias": "f", "requiresArg": true, "type": "string", "default": "./" }, - "showStackTrace": { "alias": "t", "type": "boolean", "default": false }, + "port": { "alias": "p", "requiresArg": true, "type": "number" }, + "rootFilePath": { "alias": "f", "requiresArg": true, "type": "string" }, + "showStackTrace": { "alias": "t", "type": "boolean" }, "sparqlEndpoint": { "alias": "s", "requiresArg": true, "type": "string" }, - "podConfigJson": { "requiresArg": true, "type": "string", "default": "./pod-config.json" } + "podConfigJson": { "requiresArg": true, "type": "string" } }, "options": { "usage": "node ./bin/server.js [args]" @@ -41,42 +41,47 @@ "ComputerResolver:_computers_key": "urn:solid-server:default:variable:loggingLevel", "ComputerResolver:_computers_value": { "@type": "ArgExtractor", - "argKey": "loggingLevel" + "key": "loggingLevel", + "defaultValue": "info" } }, { "ComputerResolver:_computers_key": "urn:solid-server:default:variable:port", "ComputerResolver:_computers_value": { "@type": "ArgExtractor", - "argKey": "port" + "key": "port", + "defaultValue": 3000 } }, { "ComputerResolver:_computers_key": "urn:solid-server:default:variable:rootFilePath", "ComputerResolver:_computers_value": { "@type": "AssetPathResolver", - "pathArgKey": "rootFilePath" + "key": "rootFilePath", + "defaultPath": "./" } }, { "ComputerResolver:_computers_key": "urn:solid-server:default:variable:sparqlEndpoint", "ComputerResolver:_computers_value": { "@type": "ArgExtractor", - "argKey": "sparqlEndpoint" + "key": "sparqlEndpoint" } }, { "ComputerResolver:_computers_key": "urn:solid-server:default:variable:showStackTrace", "ComputerResolver:_computers_value": { "@type": "ArgExtractor", - "argKey": "showStackTrace" + "key": "showStackTrace", + "defaultValue": false } }, { "ComputerResolver:_computers_key": "urn:solid-server:default:variable:AssetPathResolver", "ComputerResolver:_computers_value": { "@type": "AssetPathResolver", - "pathArgKey": "podConfigJson" + "key": "podConfigJson", + "defaultPath": "./pod-config.json" } } ] diff --git a/src/init/variables/computers/ArgExtractor.ts b/src/init/variables/computers/ArgExtractor.ts index d880d1d3f3..ac8573b278 100644 --- a/src/init/variables/computers/ArgExtractor.ts +++ b/src/init/variables/computers/ArgExtractor.ts @@ -4,14 +4,16 @@ import { ValueComputer } from './ValueComputer'; * Simple VarComputer that just extracts an arg from parsed args. */ export class ArgExtractor extends ValueComputer { - private readonly argKey: string; + private readonly key: string; + private readonly defaultValue: unknown; - public constructor(argKey: string) { + public constructor(key: string, defaultValue?: unknown) { super(); - this.argKey = argKey; + this.key = key; + this.defaultValue = defaultValue; } public async handle(args: Record): Promise { - return args[this.argKey]; + return typeof args[this.key] === 'undefined' ? this.defaultValue : args[this.key]; } } diff --git a/src/init/variables/computers/AssetPathResolver.ts b/src/init/variables/computers/AssetPathResolver.ts index 6092c0683d..7f2b508675 100644 --- a/src/init/variables/computers/AssetPathResolver.ts +++ b/src/init/variables/computers/AssetPathResolver.ts @@ -6,17 +6,19 @@ import { ValueComputer } from './ValueComputer'; * It follows conventions of `resolveAssetPath` function for path resolution. */ export class AssetPathResolver extends ValueComputer { - private readonly pathArgKey: string; + private readonly key: string; + private readonly defaultPath?: string; - public constructor(pathArgKey: string) { + public constructor(key: string, defaultPath?: string) { super(); - this.pathArgKey = pathArgKey; + this.key = key; + this.defaultPath = defaultPath; } public async handle(args: Record): Promise { - const path = args[this.pathArgKey]; + const path = args[this.key] ?? this.defaultPath; if (typeof path !== 'string') { - throw new Error(`Invalid ${this.pathArgKey} argument`); + throw new Error(`Invalid ${this.key} argument`); } return resolveAssetPath(path); } diff --git a/src/init/variables/computers/BaseUrlComputer.ts b/src/init/variables/computers/BaseUrlComputer.ts index 1572b54596..f4e7581813 100644 --- a/src/init/variables/computers/BaseUrlComputer.ts +++ b/src/init/variables/computers/BaseUrlComputer.ts @@ -5,7 +5,18 @@ import { ValueComputer } from './ValueComputer'; * A handler to compute base-url from args */ export class BaseUrlComputer extends ValueComputer { + private readonly defaultPort: number; + + public constructor(defaultPort = 3000) { + super(); + this.defaultPort = defaultPort; + } + public async handle(args: Record): Promise { - return typeof args.baseUrl === 'string' ? ensureTrailingSlash(args.baseUrl) : `http://localhost:${args.port}/`; + if (typeof args.baseUrl === 'string') { + return ensureTrailingSlash(args.baseUrl); + } + const port = args.port ?? this.defaultPort; + return `http://localhost:${port}/`; } } From 391bee89918e2c89eb9f9a3ef43eb326434b520a Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 14 Dec 2021 16:07:04 +0100 Subject: [PATCH 15/18] test: Add unit tests for new CLI classes --- src/init/variables/ComputerResolver.ts | 2 +- test/unit/init/AppRunner.test.ts | 282 ++++++------------ test/unit/init/cli/YargsCliExtractor.test.ts | 97 ++++++ .../init/variables/ComputerResolver.test.ts | 38 +++ .../variables/computers/ArgExtractor.test.ts | 19 ++ .../computers/AssetPathResolver.test.ts | 28 ++ .../computers/BaseUrlComputer.test.ts | 22 ++ 7 files changed, 289 insertions(+), 199 deletions(-) create mode 100644 test/unit/init/cli/YargsCliExtractor.test.ts create mode 100644 test/unit/init/variables/ComputerResolver.test.ts create mode 100644 test/unit/init/variables/computers/ArgExtractor.test.ts create mode 100644 test/unit/init/variables/computers/AssetPathResolver.test.ts create mode 100644 test/unit/init/variables/computers/BaseUrlComputer.test.ts diff --git a/src/init/variables/ComputerResolver.ts b/src/init/variables/ComputerResolver.ts index 0d0413e975..d5355f2e3b 100644 --- a/src/init/variables/ComputerResolver.ts +++ b/src/init/variables/ComputerResolver.ts @@ -17,7 +17,7 @@ export class ComputerResolver extends VariableResolver { const vars: Record = {}; for (const [ name, computer ] of Object.entries(this.computers)) { try { - vars[name] = await computer.handle(input); + vars[name] = await computer.handleSafe(input); } catch (err: unknown) { throw new Error(`Error in computing value for variable ${name}: ${createErrorMessage(err)}`); } diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index ec206e4df7..4db4eee60a 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -1,14 +1,38 @@ import { ComponentsManager } from 'componentsjs'; import type { App } from '../../../src/init/App'; import { AppRunner } from '../../../src/init/AppRunner'; +import type { CliExtractor } from '../../../src/init/cli/CliExtractor'; +import type { VariableResolver } from '../../../src/init/variables/VariableResolver'; import { joinFilePath } from '../../../src/util/PathUtil'; const app: jest.Mocked = { start: jest.fn(), } as any; +const defaultParameters = { + port: 3000, + logLevel: 'info', +}; +const extractor: jest.Mocked = { + handleSafe: jest.fn().mockResolvedValue(defaultParameters), +} as any; + +const defaultVariables = { + 'urn:solid-server:default:variable:port': 3000, + 'urn:solid-server:default:variable:loggingLevel': 'info', +}; +const resolver: jest.Mocked = { + handleSafe: jest.fn().mockResolvedValue(defaultVariables), +} as any; + const manager: jest.Mocked> = { - instantiate: jest.fn(async(): Promise => app), + instantiate: jest.fn(async(iri: string): Promise => { + switch (iri) { + case 'urn:solid-server-app-setup:default:CliResolver': return { extractor, resolver }; + case 'urn:solid-server:default:App': return app; + default: throw new Error('unknown iri'); + } + }), configRegistry: { register: jest.fn(), }, @@ -22,7 +46,6 @@ jest.mock('componentsjs', (): any => ({ })); jest.spyOn(process, 'cwd').mockReturnValue('/var/cwd'); -const error = jest.spyOn(console, 'error').mockImplementation(jest.fn()); const write = jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn()); const exit = jest.spyOn(process, 'exit').mockImplementation(jest.fn() as any); @@ -32,7 +55,14 @@ describe('AppRunner', (): void => { }); describe('run', (): void => { - it('starts the server with default settings.', async(): Promise => { + it('starts the server with provided settings.', async(): Promise => { + const parameters = { + port: 3000, + loggingLevel: 'info', + rootFilePath: '/var/cwd/', + showStackTrace: false, + podConfigJson: '/var/cwd/pod-config.json', + }; await new AppRunner().run( { mainModulePath: joinFilePath(__dirname, '../../../'), @@ -40,13 +70,7 @@ describe('AppRunner', (): void => { logLevel: 'info', }, joinFilePath(__dirname, '../../../config/default.json'), - { - port: 3000, - loggingLevel: 'info', - rootFilePath: '/var/cwd/', - showStackTrace: false, - podConfigJson: '/var/cwd/pod-config.json', - }, + parameters, ); expect(ComponentsManager.build).toHaveBeenCalledTimes(1); @@ -58,21 +82,14 @@ describe('AppRunner', (): void => { expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register) .toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json')); - expect(manager.instantiate).toHaveBeenCalledTimes(1); - expect(manager.instantiate).toHaveBeenCalledWith( + expect(manager.instantiate).toHaveBeenCalledTimes(2); + expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {}); + expect(extractor.handleSafe).toHaveBeenCalledTimes(0); + expect(resolver.handleSafe).toHaveBeenCalledTimes(1); + expect(resolver.handleSafe).toHaveBeenCalledWith(parameters); + expect(manager.instantiate).toHaveBeenNthCalledWith(2, 'urn:solid-server:default:App', - { - variables: { - 'urn:solid-server:default:variable:port': 3000, - 'urn:solid-server:default:variable:baseUrl': 'http://localhost:3000/', - 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/', - 'urn:solid-server:default:variable:sparqlEndpoint': undefined, - 'urn:solid-server:default:variable:loggingLevel': 'info', - 'urn:solid-server:default:variable:showStackTrace': false, - 'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json', - }, - }, - ); + { variables: defaultVariables }); expect(app.start).toHaveBeenCalledTimes(1); expect(app.start).toHaveBeenCalledWith(); }); @@ -98,133 +115,22 @@ describe('AppRunner', (): void => { expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register) .toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json')); - expect(manager.instantiate).toHaveBeenCalledTimes(1); - expect(manager.instantiate).toHaveBeenCalledWith( + expect(manager.instantiate).toHaveBeenCalledTimes(2); + expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {}); + expect(extractor.handleSafe).toHaveBeenCalledTimes(1); + expect(extractor.handleSafe).toHaveBeenCalledWith([ 'node', 'script' ]); + expect(resolver.handleSafe).toHaveBeenCalledTimes(1); + expect(resolver.handleSafe).toHaveBeenCalledWith(defaultParameters); + expect(manager.instantiate).toHaveBeenNthCalledWith(2, 'urn:solid-server:default:App', - { - variables: { - 'urn:solid-server:default:variable:port': 3000, - 'urn:solid-server:default:variable:baseUrl': 'http://localhost:3000/', - 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/', - 'urn:solid-server:default:variable:sparqlEndpoint': undefined, - 'urn:solid-server:default:variable:loggingLevel': 'info', - 'urn:solid-server:default:variable:showStackTrace': false, - 'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json', - }, - }, - ); + { variables: defaultVariables }); expect(app.start).toHaveBeenCalledTimes(1); expect(app.start).toHaveBeenCalledWith(); }); - it('accepts abbreviated flags.', async(): Promise => { - new AppRunner().runCli({ - argv: [ - 'node', 'script', - '-b', 'http://pod.example/', - '-c', 'myconfig.json', - '-f', '/root', - '-l', 'debug', - '-m', 'module/path', - '-p', '4000', - '-s', 'http://localhost:5000/sparql', - '-t', - '--podConfigJson', '/different-path.json', - ], - }); - - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - - expect(ComponentsManager.build).toHaveBeenCalledTimes(1); - expect(ComponentsManager.build).toHaveBeenCalledWith({ - dumpErrorState: true, - logLevel: 'debug', - mainModulePath: '/var/cwd/module/path', - }); - expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); - expect(manager.configRegistry.register).toHaveBeenCalledWith('/var/cwd/myconfig.json'); - expect(manager.instantiate).toHaveBeenCalledWith( - 'urn:solid-server:default:App', - { - variables: { - 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', - 'urn:solid-server:default:variable:loggingLevel': 'debug', - 'urn:solid-server:default:variable:port': 4000, - 'urn:solid-server:default:variable:rootFilePath': '/root', - 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql', - 'urn:solid-server:default:variable:showStackTrace': true, - 'urn:solid-server:default:variable:podConfigJson': '/different-path.json', - }, - }, - ); - }); - - it('accepts full flags.', async(): Promise => { - new AppRunner().runCli({ - argv: [ - 'node', 'script', - '--baseUrl', 'http://pod.example/', - '--config', 'myconfig.json', - '--loggingLevel', 'debug', - '--mainModulePath', 'module/path', - '--port', '4000', - '--rootFilePath', 'root', - '--sparqlEndpoint', 'http://localhost:5000/sparql', - '--showStackTrace', - '--podConfigJson', '/different-path.json', - ], - }); - - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); - - expect(ComponentsManager.build).toHaveBeenCalledTimes(1); - expect(ComponentsManager.build).toHaveBeenCalledWith({ - dumpErrorState: true, - logLevel: 'debug', - mainModulePath: '/var/cwd/module/path', - }); - expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); - expect(manager.configRegistry.register).toHaveBeenCalledWith('/var/cwd/myconfig.json'); - expect(manager.instantiate).toHaveBeenCalledWith( - 'urn:solid-server:default:App', - { - variables: { - 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', - 'urn:solid-server:default:variable:loggingLevel': 'debug', - 'urn:solid-server:default:variable:port': 4000, - 'urn:solid-server:default:variable:rootFilePath': '/var/cwd/root', - 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql', - 'urn:solid-server:default:variable:showStackTrace': true, - 'urn:solid-server:default:variable:podConfigJson': '/different-path.json', - }, - }, - ); - }); - - it('accepts asset paths for the config flag.', async(): Promise => { - new AppRunner().runCli({ - argv: [ - 'node', 'script', - '--config', '@css:config/file.json', - ], - }); - await new Promise(setImmediate); - - expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); - expect(manager.configRegistry.register).toHaveBeenCalledWith( - joinFilePath(__dirname, '../../../config/file.json'), - ); - }); - it('uses the default process.argv in case none are provided.', async(): Promise => { const { argv } = process; - process.argv = [ + const argvParameters = [ 'node', 'script', '-b', 'http://pod.example/', '-c', 'myconfig.json', @@ -236,6 +142,7 @@ describe('AppRunner', (): void => { '-t', '--podConfigJson', '/different-path.json', ]; + process.argv = argvParameters; new AppRunner().runCli(); @@ -252,26 +159,23 @@ describe('AppRunner', (): void => { }); expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); expect(manager.configRegistry.register).toHaveBeenCalledWith('/var/cwd/myconfig.json'); - expect(manager.instantiate).toHaveBeenCalledWith( + expect(manager.instantiate).toHaveBeenCalledTimes(2); + expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {}); + expect(extractor.handleSafe).toHaveBeenCalledTimes(1); + expect(extractor.handleSafe).toHaveBeenCalledWith(argvParameters); + expect(resolver.handleSafe).toHaveBeenCalledTimes(1); + expect(resolver.handleSafe).toHaveBeenCalledWith(defaultParameters); + expect(manager.instantiate).toHaveBeenNthCalledWith(2, 'urn:solid-server:default:App', - { - variables: { - 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', - 'urn:solid-server:default:variable:loggingLevel': 'debug', - 'urn:solid-server:default:variable:port': 4000, - 'urn:solid-server:default:variable:rootFilePath': '/root', - 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql', - 'urn:solid-server:default:variable:showStackTrace': true, - 'urn:solid-server:default:variable:podConfigJson': '/different-path.json', - }, - }, - ); + { variables: defaultVariables }); + expect(app.start).toHaveBeenCalledTimes(1); + expect(app.start).toHaveBeenCalledWith(); process.argv = argv; }); - it('exits with output to stderr when instantiation fails.', async(): Promise => { - manager.instantiate.mockRejectedValueOnce(new Error('Fatal')); + it('exits with output to stderr when creating a ComponentsManager fails.', async(): Promise => { + (manager.configRegistry.register as jest.Mock).mockRejectedValueOnce(new Error('Fatal')); new AppRunner().runCli({ argv: [ 'node', 'script' ], }); @@ -283,7 +187,7 @@ describe('AppRunner', (): void => { expect(write).toHaveBeenCalledTimes(2); expect(write).toHaveBeenNthCalledWith(1, - expect.stringMatching(/^Error: could not instantiate server from .*default\.json/u)); + expect.stringMatching(/^Could not build the config files from .*default\.json/u)); expect(write).toHaveBeenNthCalledWith(2, expect.stringMatching(/^Error: Fatal/u)); @@ -291,8 +195,8 @@ describe('AppRunner', (): void => { expect(exit).toHaveBeenCalledWith(1); }); - it('exits without output to stderr when initialization fails.', async(): Promise => { - app.start.mockRejectedValueOnce(new Error('Fatal')); + it('exits with output to stderr when instantiation fails.', async(): Promise => { + manager.instantiate.mockRejectedValueOnce(new Error('Fatal')); new AppRunner().runCli({ argv: [ 'node', 'script' ], }); @@ -302,29 +206,20 @@ describe('AppRunner', (): void => { setImmediate(resolve); }); - expect(write).toHaveBeenCalledTimes(0); - - expect(exit).toHaveBeenCalledWith(1); - }); - - it('exits when unknown options are passed to the main executable.', async(): Promise => { - new AppRunner().runCli({ - argv: [ 'node', 'script', '--foo' ], - }); - - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); + expect(write).toHaveBeenCalledTimes(2); + expect(write).toHaveBeenNthCalledWith(1, + expect.stringMatching(/^Could not load config variables from .*default\.json/u)); + expect(write).toHaveBeenNthCalledWith(2, + expect.stringMatching(/^Error: Fatal/u)); - expect(error).toHaveBeenCalledWith('Unknown argument: foo'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); - it('exits when no value is passed to the main executable for an argument.', async(): Promise => { + it('exits with output to stderr when no CliExtractor is defined.', async(): Promise => { + manager.instantiate.mockResolvedValueOnce({ resolver }); new AppRunner().runCli({ - argv: [ 'node', 'script', '-s' ], + argv: [ 'node', 'script' ], }); // Wait until app.start has been called, because we can't await AppRunner.run. @@ -332,29 +227,20 @@ describe('AppRunner', (): void => { setImmediate(resolve); }); - expect(error).toHaveBeenCalledWith('Not enough arguments following: s'); - expect(exit).toHaveBeenCalledTimes(1); - expect(exit).toHaveBeenCalledWith(1); - }); - - it('exits when unknown parameters are passed to the main executable.', async(): Promise => { - new AppRunner().runCli({ - argv: [ 'node', 'script', 'foo', 'bar', 'foo.txt', 'bar.txt' ], - }); - - // Wait until app.start has been called, because we can't await AppRunner.run. - await new Promise((resolve): void => { - setImmediate(resolve); - }); + expect(write).toHaveBeenCalledTimes(2); + expect(write).toHaveBeenNthCalledWith(1, + expect.stringMatching(/^Could not load config variables from .*default\.json/u)); + expect(write).toHaveBeenNthCalledWith(2, + expect.stringMatching(/^Error: No CliExtractor is defined/u)); - expect(error).toHaveBeenCalledWith('Unsupported positional arguments: "foo", "bar", "foo.txt", "bar.txt"'); expect(exit).toHaveBeenCalledTimes(1); expect(exit).toHaveBeenCalledWith(1); }); - it('exits when multiple values for a parameter are passed.', async(): Promise => { + it('exits without output to stderr when initialization fails.', async(): Promise => { + app.start.mockRejectedValueOnce(new Error('Fatal')); new AppRunner().runCli({ - argv: [ 'node', 'script', '-l', 'info', '-l', 'debug' ], + argv: [ 'node', 'script' ], }); // Wait until app.start has been called, because we can't await AppRunner.run. @@ -362,8 +248,8 @@ describe('AppRunner', (): void => { setImmediate(resolve); }); - expect(error).toHaveBeenCalledWith('Multiple values were provided for: "l": "info", "debug"'); - expect(exit).toHaveBeenCalledTimes(1); + expect(write).toHaveBeenCalledTimes(0); + expect(exit).toHaveBeenCalledWith(1); }); }); diff --git a/test/unit/init/cli/YargsCliExtractor.test.ts b/test/unit/init/cli/YargsCliExtractor.test.ts new file mode 100644 index 0000000000..90393d9f76 --- /dev/null +++ b/test/unit/init/cli/YargsCliExtractor.test.ts @@ -0,0 +1,97 @@ +import type { YargsArgOptions } from '../../../../src/init/cli/YargsCliExtractor'; +import { YargsCliExtractor } from '../../../../src/init/cli/YargsCliExtractor'; + +const error = jest.spyOn(console, 'error').mockImplementation(jest.fn()); +const log = jest.spyOn(console, 'log').mockImplementation(jest.fn()); +const exit = jest.spyOn(process, 'exit').mockImplementation(jest.fn() as any); +describe('A YargsCliExtractor', (): void => { + const parameters: YargsArgOptions = { + baseUrl: { alias: 'b', requiresArg: true, type: 'string' }, + port: { alias: 'p', requiresArg: true, type: 'number' }, + }; + let extractor: YargsCliExtractor; + + beforeEach(async(): Promise => { + extractor = new YargsCliExtractor(parameters); + }); + + afterEach(async(): Promise => { + jest.clearAllMocks(); + }); + + it('returns parsed results.', async(): Promise => { + const argv = [ 'node', 'script', '-b', 'http://localhost:3000/', '-p', '3000' ]; + await expect(extractor.handle(argv)).resolves.toEqual(expect.objectContaining({ + baseUrl: 'http://localhost:3000/', + port: 3000, + })); + }); + + it('accepts full flags.', async(): Promise => { + const argv = [ 'node', 'script', '--baseUrl', 'http://localhost:3000/', '--port', '3000' ]; + await expect(extractor.handle(argv)).resolves.toEqual(expect.objectContaining({ + baseUrl: 'http://localhost:3000/', + port: 3000, + })); + }); + + it('defaults to no parameters if none are provided.', async(): Promise => { + extractor = new YargsCliExtractor(); + const argv = [ 'node', 'script', '-b', 'http://localhost:3000/', '-p', '3000' ]; + await expect(extractor.handle(argv)).resolves.toEqual(expect.objectContaining({})); + }); + + it('prints usage if defined.', async(): Promise => { + extractor = new YargsCliExtractor(parameters, { usage: 'node ./bin/server.js [args]' }); + const argv = [ 'node', 'script', '--help' ]; + await extractor.handle(argv); + expect(exit).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenLastCalledWith(expect.stringMatching(/^node \.\/bin\/server\.js \[args\]/u)); + }); + + it('can error on undefined parameters.', async(): Promise => { + extractor = new YargsCliExtractor(parameters, { strictMode: true }); + const argv = [ 'node', 'script', '--unsupported' ]; + await extractor.handle(argv); + expect(exit).toHaveBeenCalledTimes(1); + expect(error).toHaveBeenCalledWith('Unknown argument: unsupported'); + }); + + it('can parse environment variables.', async(): Promise => { + // While the code below does go into the corresponding values, + // yargs does not see the new environment variable for some reason. + // It does see all the env variables that were already in there + // (which can be tested by setting envVarPrefix to ''). + // This can probably be fixed by changing jest setup to already load the custom env before loading the tests, + // but does not seem worth it just for this test. + const { env } = process; + // eslint-disable-next-line @typescript-eslint/naming-convention + process.env = { ...env, TEST_ENV_PORT: '3333' }; + extractor = new YargsCliExtractor(parameters, { loadFromEnv: true, envVarPrefix: 'TEST_ENV' }); + const argv = [ 'node', 'script', '-b', 'http://localhost:3333/' ]; + await expect(extractor.handle(argv)).resolves.toEqual(expect.objectContaining({ + baseUrl: 'http://localhost:3333/', + })); + process.env = env; + + // This part is here for the case of envVarPrefix being defined + // since it doesn't make much sense to test it if the above doesn't work + extractor = new YargsCliExtractor(parameters, { loadFromEnv: true }); + await extractor.handle(argv); + }); + + it('errors when positional arguments are passed.', async(): Promise => { + const argv = [ 'node', 'script', 'unsupported.txt' ]; + await extractor.handle(argv); + expect(exit).toHaveBeenCalledTimes(1); + expect(error).toHaveBeenCalledWith('Unsupported positional arguments: "unsupported.txt"'); + }); + + it('errors when there are multiple values for the same argument.', async(): Promise => { + const argv = [ 'node', 'script', '-p', '3000', '-p', '2000' ]; + await extractor.handle(argv); + expect(exit).toHaveBeenCalledTimes(1); + expect(error).toHaveBeenCalledWith('Multiple values were provided for: "p": "3000", "2000"'); + }); +}); diff --git a/test/unit/init/variables/ComputerResolver.test.ts b/test/unit/init/variables/ComputerResolver.test.ts new file mode 100644 index 0000000000..8616a15d71 --- /dev/null +++ b/test/unit/init/variables/ComputerResolver.test.ts @@ -0,0 +1,38 @@ +import { ComputerResolver } from '../../../../src/init/variables/ComputerResolver'; +import type { ValueComputer } from '../../../../src/init/variables/computers/ValueComputer'; + +describe('A ComputerResolver', (): void => { + const values = { test: 'data' }; + const varPort = 'urn:solid-server:default:variable:port'; + const varLog = 'urn:solid-server:default:variable:loggingLevel'; + let computerPort: jest.Mocked; + let computerLog: jest.Mocked; + let resolver: ComputerResolver; + + beforeEach(async(): Promise => { + computerPort = { + handleSafe: jest.fn().mockResolvedValue(3000), + } as any; + + computerLog = { + handleSafe: jest.fn().mockResolvedValue('info'), + } as any; + + resolver = new ComputerResolver({ + [varPort]: computerPort, + [varLog]: computerLog, + }); + }); + + it('assigns variable values based on the Computers output.', async(): Promise => { + await expect(resolver.handle(values)).resolves.toEqual({ + [varPort]: 3000, + [varLog]: 'info', + }); + }); + + it('rethrows the error if something goes wrong.', async(): Promise => { + computerPort.handleSafe.mockRejectedValueOnce(new Error('bad data')); + await expect(resolver.handle(values)).rejects.toThrow(`Error in computing value for variable ${varPort}: bad data`); + }); +}); diff --git a/test/unit/init/variables/computers/ArgExtractor.test.ts b/test/unit/init/variables/computers/ArgExtractor.test.ts new file mode 100644 index 0000000000..54587a64d0 --- /dev/null +++ b/test/unit/init/variables/computers/ArgExtractor.test.ts @@ -0,0 +1,19 @@ +import { ArgExtractor } from '../../../../../src/init/variables/computers/ArgExtractor'; + +describe('An ArgExtractor', (): void => { + const key = 'test'; + let extractor: ArgExtractor; + + beforeEach(async(): Promise => { + extractor = new ArgExtractor(key); + }); + + it('extracts the value.', async(): Promise => { + await expect(extractor.handle({ test: 'data', notTest: 'notData' })).resolves.toEqual('data'); + }); + + it('defaults to a given value if none is defined.', async(): Promise => { + extractor = new ArgExtractor(key, 'defaultData'); + await expect(extractor.handle({ notTest: 'notData' })).resolves.toEqual('defaultData'); + }); +}); diff --git a/test/unit/init/variables/computers/AssetPathResolver.test.ts b/test/unit/init/variables/computers/AssetPathResolver.test.ts new file mode 100644 index 0000000000..3a1a8d7bf0 --- /dev/null +++ b/test/unit/init/variables/computers/AssetPathResolver.test.ts @@ -0,0 +1,28 @@ +import { AssetPathResolver } from '../../../../../src/init/variables/computers/AssetPathResolver'; +import { joinFilePath } from '../../../../../src/util/PathUtil'; + +describe('An AssetPathResolver', (): void => { + let resolver: AssetPathResolver; + + beforeEach(async(): Promise => { + resolver = new AssetPathResolver('path'); + }); + + it('resolves the asset path.', async(): Promise => { + await expect(resolver.handle({ path: '/var/data' })).resolves.toEqual('/var/data'); + }); + + it('errors if the path is not a string.', async(): Promise => { + await expect(resolver.handle({ path: 1234 })).rejects.toThrow('Invalid path argument'); + }); + + it('converts paths containing the module path placeholder.', async(): Promise => { + await expect(resolver.handle({ path: '@css:config/file.json' })) + .resolves.toEqual(joinFilePath(__dirname, '../../../../../config/file.json')); + }); + + it('defaults to the given path if none is provided.', async(): Promise => { + resolver = new AssetPathResolver('path', '/root'); + await expect(resolver.handle({ otherPath: '/var/data' })).resolves.toEqual('/root'); + }); +}); diff --git a/test/unit/init/variables/computers/BaseUrlComputer.test.ts b/test/unit/init/variables/computers/BaseUrlComputer.test.ts new file mode 100644 index 0000000000..0a478558b7 --- /dev/null +++ b/test/unit/init/variables/computers/BaseUrlComputer.test.ts @@ -0,0 +1,22 @@ +import { BaseUrlComputer } from '../../../../../src/init/variables/computers/BaseUrlComputer'; + +describe('A BaseUrlComputer', (): void => { + let computer: BaseUrlComputer; + + beforeEach(async(): Promise => { + computer = new BaseUrlComputer(); + }); + + it('extracts the baseUrl parameter.', async(): Promise => { + await expect(computer.handle({ baseUrl: 'http://example.com/', port: 3333 })) + .resolves.toEqual('http://example.com/'); + }); + + it('uses the port parameter if baseUrl is not defined.', async(): Promise => { + await expect(computer.handle({ port: 3333 })).resolves.toEqual('http://localhost:3333/'); + }); + + it('defaults to port 3000.', async(): Promise => { + await expect(computer.handle({})).resolves.toEqual('http://localhost:3000/'); + }); +}); From affafa97dd72255b2cee4c17c5df46a4390643b2 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 14 Dec 2021 17:05:16 +0100 Subject: [PATCH 16/18] feat: Integrate new CLI configuration with all default configurations --- RELEASE_NOTES.md | 5 +- config/app-setup/vars.json | 93 --------------------- config/app/README.md | 4 + config/app/variables/cli/cli.json | 22 +++++ config/app/variables/default.json | 28 +++++++ config/app/variables/resolver/resolver.json | 65 ++++++++++++++ config/default.json | 3 +- config/dynamic.json | 1 + config/example-https-file.json | 1 + config/file-no-setup.json | 1 + config/file.json | 1 + config/memory-subdomains.json | 1 + config/path-routing.json | 1 + config/restrict-idp.json | 1 + config/sparql-endpoint-no-setup.json | 1 + config/sparql-endpoint.json | 1 + 16 files changed, 133 insertions(+), 96 deletions(-) delete mode 100644 config/app-setup/vars.json create mode 100644 config/app/variables/cli/cli.json create mode 100644 config/app/variables/default.json create mode 100644 config/app/variables/resolver/resolver.json diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 70bb6acde1..3d691a35bd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,12 +3,15 @@ ## v3.0.0 ### New features - The Identity Provider now uses the `webid` scope as required for Solid-OIDC. +- It is now possible to modify how CLI variables are parsed. ### Configuration changes You might need to make changes to your v2 configuration if you use a custom config. The following changes pertain to the imports in the default configs: -- ... +- There is a new configuration option that needs to be imported: + - `/app/variables/default/json` contains everything related to parsing CLI arguments + and assigning values to variables. The following changes are relevant for v2 custom configs that replaced certain features. - Conversion has been simplified so most converters are part of the conversion chain: diff --git a/config/app-setup/vars.json b/config/app-setup/vars.json deleted file mode 100644 index 7bbb7a04a6..0000000000 --- a/config/app-setup/vars.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "@context": [ - "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld" - ], - "@graph": [ - { - "comment": "VarResolver resolves variables to be used in app instantiation, from cli args", - "@id": "urn:solid-server-app-setup:default:CliResolver", - "@type": "RecordObject", - "record": [ - { - "RecordObject:_record_key": "extractor", - "RecordObject:_record_value": { - "@type": "YargsCliExtractor", - "parameters": { - "loggingLevel": { "alias": "l", "requiresArg": true, "type": "string" }, - "baseUrl": { "alias": "b", "requiresArg": true, "type": "string" }, - "port": { "alias": "p", "requiresArg": true, "type": "number" }, - "rootFilePath": { "alias": "f", "requiresArg": true, "type": "string" }, - "showStackTrace": { "alias": "t", "type": "boolean" }, - "sparqlEndpoint": { "alias": "s", "requiresArg": true, "type": "string" }, - "podConfigJson": { "requiresArg": true, "type": "string" } - }, - "options": { - "usage": "node ./bin/server.js [args]" - } - } - }, - { - "RecordObject:_record_key": "resolver", - "RecordObject:_record_value": { - "@type": "ComputerResolver", - "computers": [ - { - "ComputerResolver:_computers_key": "urn:solid-server:default:variable:baseUrl", - "ComputerResolver:_computers_value": { - "@type": "BaseUrlComputer" - } - }, - { - "ComputerResolver:_computers_key": "urn:solid-server:default:variable:loggingLevel", - "ComputerResolver:_computers_value": { - "@type": "ArgExtractor", - "key": "loggingLevel", - "defaultValue": "info" - } - }, - { - "ComputerResolver:_computers_key": "urn:solid-server:default:variable:port", - "ComputerResolver:_computers_value": { - "@type": "ArgExtractor", - "key": "port", - "defaultValue": 3000 - } - }, - { - "ComputerResolver:_computers_key": "urn:solid-server:default:variable:rootFilePath", - "ComputerResolver:_computers_value": { - "@type": "AssetPathResolver", - "key": "rootFilePath", - "defaultPath": "./" - } - }, - { - "ComputerResolver:_computers_key": "urn:solid-server:default:variable:sparqlEndpoint", - "ComputerResolver:_computers_value": { - "@type": "ArgExtractor", - "key": "sparqlEndpoint" - } - }, - { - "ComputerResolver:_computers_key": "urn:solid-server:default:variable:showStackTrace", - "ComputerResolver:_computers_value": { - "@type": "ArgExtractor", - "key": "showStackTrace", - "defaultValue": false - } - }, - { - "ComputerResolver:_computers_key": "urn:solid-server:default:variable:AssetPathResolver", - "ComputerResolver:_computers_value": { - "@type": "AssetPathResolver", - "key": "podConfigJson", - "defaultPath": "./pod-config.json" - } - } - ] - } - } - ] - } - ] -} diff --git a/config/app/README.md b/config/app/README.md index 1e7b2965af..70965ecbfc 100644 --- a/config/app/README.md +++ b/config/app/README.md @@ -20,3 +20,7 @@ Handles the setup page the first time the server is started. * *optional*: Setup is available at `/setup` but the server can already be used. Everyone can access the setup page so make sure to complete that as soon as possible. * *required*: All requests will be redirected to the setup page until setup is completed. + +## Variables +Handles parsing CLI parameters and assigning values to Components.js variables. +* *default*: Assigns CLI parameters for all variables defined in `/config/util/variables/default.json` diff --git a/config/app/variables/cli/cli.json b/config/app/variables/cli/cli.json new file mode 100644 index 0000000000..e0add45a82 --- /dev/null +++ b/config/app/variables/cli/cli.json @@ -0,0 +1,22 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Extracts CLI arguments into a key/value object", + "@id": "urn:solid-server-app-setup:default:CliExtractor", + "@type": "YargsCliExtractor", + "parameters": { + "loggingLevel": { "alias": "l", "requiresArg": true, "type": "string" }, + "baseUrl": { "alias": "b", "requiresArg": true, "type": "string" }, + "port": { "alias": "p", "requiresArg": true, "type": "number" }, + "rootFilePath": { "alias": "f", "requiresArg": true, "type": "string" }, + "showStackTrace": { "alias": "t", "type": "boolean" }, + "sparqlEndpoint": { "alias": "s", "requiresArg": true, "type": "string" }, + "podConfigJson": { "requiresArg": true, "type": "string" } + }, + "options": { + "usage": "node ./bin/server.js [args]" + } + } + ] +} diff --git a/config/app/variables/default.json b/config/app/variables/default.json new file mode 100644 index 0000000000..540cb1e9bc --- /dev/null +++ b/config/app/variables/default.json @@ -0,0 +1,28 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld", + "import": [ + "files-scs:config/app/variables/cli/cli.json", + "files-scs:config/app/variables/resolver/resolver.json" + ], + "@graph": [ + { + "comment": "Combines a CliExtractor and VariableResolver to be used by the AppRunner.", + "@id": "urn:solid-server-app-setup:default:CliResolver", + "@type": "RecordObject", + "record": [ + { + "RecordObject:_record_key": "extractor", + "RecordObject:_record_value": { + "@id": "urn:solid-server-app-setup:default:CliExtractor" + } + }, + { + "RecordObject:_record_key": "resolver", + "RecordObject:_record_value": { + "@id": "urn:solid-server-app-setup:default:VariableResolver" + } + } + ] + } + ] +} diff --git a/config/app/variables/resolver/resolver.json b/config/app/variables/resolver/resolver.json new file mode 100644 index 0000000000..1ee34c3446 --- /dev/null +++ b/config/app/variables/resolver/resolver.json @@ -0,0 +1,65 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Converts an input key/value object into an object mapping values to Components.js variables", + "@id": "urn:solid-server-app-setup:default:VariableResolver", + "@type": "ComputerResolver", + "computers": [ + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:baseUrl", + "ComputerResolver:_computers_value": { + "@type": "BaseUrlComputer" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:loggingLevel", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "key": "loggingLevel", + "defaultValue": "info" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:port", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "key": "port", + "defaultValue": 3000 + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:rootFilePath", + "ComputerResolver:_computers_value": { + "@type": "AssetPathResolver", + "key": "rootFilePath", + "defaultPath": "./" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:sparqlEndpoint", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "key": "sparqlEndpoint" + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:showStackTrace", + "ComputerResolver:_computers_value": { + "@type": "ArgExtractor", + "key": "showStackTrace", + "defaultValue": false + } + }, + { + "ComputerResolver:_computers_key": "urn:solid-server:default:variable:AssetPathResolver", + "ComputerResolver:_computers_value": { + "@type": "AssetPathResolver", + "key": "podConfigJson", + "defaultPath": "./pod-config.json" + } + } + ] + } + ] +} diff --git a/config/default.json b/config/default.json index 4d6799a18d..7098564480 100644 --- a/config/default.json +++ b/config/default.json @@ -1,11 +1,10 @@ { "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^2.0.0/components/context.jsonld", "import": [ - "files-scs:config/app-setup/vars.json", - "files-scs:config/app/main/default.json", "files-scs:config/app/init/initialize-prefilled-root.json", "files-scs:config/app/setup/optional.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/dynamic.json b/config/dynamic.json index d6552ca800..d731158e9a 100644 --- a/config/dynamic.json +++ b/config/dynamic.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/default.json", "files-scs:config/app/setup/required.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/example-https-file.json b/config/example-https-file.json index 77b2163a3c..8ec5248bc9 100644 --- a/config/example-https-file.json +++ b/config/example-https-file.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/default.json", "files-scs:config/app/setup/required.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", diff --git a/config/file-no-setup.json b/config/file-no-setup.json index 26ff5f0ba0..b4c5096a28 100644 --- a/config/file-no-setup.json +++ b/config/file-no-setup.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/initialize-root.json", "files-scs:config/app/setup/disabled.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/file.json b/config/file.json index 4e220273a2..40948c7ede 100644 --- a/config/file.json +++ b/config/file.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/default.json", "files-scs:config/app/setup/required.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/memory-subdomains.json b/config/memory-subdomains.json index fef343dd27..bb1d61362b 100644 --- a/config/memory-subdomains.json +++ b/config/memory-subdomains.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/initialize-root.json", "files-scs:config/app/setup/optional.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/path-routing.json b/config/path-routing.json index f05e988335..50296f7105 100644 --- a/config/path-routing.json +++ b/config/path-routing.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/initialize-root.json", "files-scs:config/app/setup/disabled.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/restrict-idp.json b/config/restrict-idp.json index 4ad5edfd25..f531031d9a 100644 --- a/config/restrict-idp.json +++ b/config/restrict-idp.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/default.json", "files-scs:config/app/setup/disabled.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/sparql-endpoint-no-setup.json b/config/sparql-endpoint-no-setup.json index 4f4f9731d0..04ea8ebbe1 100644 --- a/config/sparql-endpoint-no-setup.json +++ b/config/sparql-endpoint-no-setup.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/initialize-root.json", "files-scs:config/app/setup/disabled.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", diff --git a/config/sparql-endpoint.json b/config/sparql-endpoint.json index af64b61865..bf63d0453c 100644 --- a/config/sparql-endpoint.json +++ b/config/sparql-endpoint.json @@ -4,6 +4,7 @@ "files-scs:config/app/main/default.json", "files-scs:config/app/init/default.json", "files-scs:config/app/setup/required.json", + "files-scs:config/app/variables/default.json", "files-scs:config/http/handler/default.json", "files-scs:config/http/middleware/websockets.json", "files-scs:config/http/server-factory/websockets.json", From 660f72871217813afcd44b7049f9b0c15f2b2cf4 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 16 Dec 2021 10:04:14 +0100 Subject: [PATCH 17/18] feat: Add createApp function to AppRunner --- src/init/AppRunner.ts | 23 ++++++++++++++---- test/unit/init/AppRunner.test.ts | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 40cfbe815a..bb3e9b92c6 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -32,6 +32,23 @@ interface CliResolver { export class AppRunner { private readonly logger = getLoggerFor(this); + /** + * Returns an App object, created with the given config, that can start and stop the Solid server. + * @param loaderProperties - Components.js loader properties. + * @param configFile - Path to the server config file. + * @param parameters - Parameters to pass into the VariableResolver. + */ + public async createApp( + loaderProperties: IComponentsManagerBuilderOptions, + configFile: string, + parameters: Record, + ): Promise { + const componentsManager = await this.createComponentsManager(loaderProperties, configFile); + const resolver = await componentsManager.instantiate(DEFAULT_CLI_RESOLVER, {}); + const variables = await resolver.resolver.handleSafe(parameters); + return componentsManager.instantiate(DEFAULT_APP, { variables }); + } + /** * Starts the server with a given config. * This method can be used to start the server from within another JavaScript application. @@ -44,10 +61,8 @@ export class AppRunner { configFile: string, parameters: Record, ): Promise { - const componentsManager = await this.createComponentsManager(loaderProperties, configFile); - const resolver = await componentsManager.instantiate(DEFAULT_CLI_RESOLVER, {}); - const variables = await resolver.resolver.handleSafe(parameters); - await this.startApp(componentsManager, variables); + const app = await this.createApp(loaderProperties, configFile, parameters); + await app.start(); } /** diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index 4db4eee60a..5208599261 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -54,6 +54,47 @@ describe('AppRunner', (): void => { jest.clearAllMocks(); }); + describe('createApp', (): void => { + it('creates an App with the provided settings.', async(): Promise => { + const parameters = { + port: 3000, + loggingLevel: 'info', + rootFilePath: '/var/cwd/', + showStackTrace: false, + podConfigJson: '/var/cwd/pod-config.json', + }; + const createdApp = await new AppRunner().createApp( + { + mainModulePath: joinFilePath(__dirname, '../../../'), + dumpErrorState: true, + logLevel: 'info', + }, + joinFilePath(__dirname, '../../../config/default.json'), + parameters, + ); + expect(createdApp).toBe(app); + + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); + expect(ComponentsManager.build).toHaveBeenCalledWith({ + dumpErrorState: true, + logLevel: 'info', + mainModulePath: joinFilePath(__dirname, '../../../'), + }); + expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); + expect(manager.configRegistry.register) + .toHaveBeenCalledWith(joinFilePath(__dirname, '/../../../config/default.json')); + expect(manager.instantiate).toHaveBeenCalledTimes(2); + expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {}); + expect(extractor.handleSafe).toHaveBeenCalledTimes(0); + expect(resolver.handleSafe).toHaveBeenCalledTimes(1); + expect(resolver.handleSafe).toHaveBeenCalledWith(parameters); + expect(manager.instantiate).toHaveBeenNthCalledWith(2, + 'urn:solid-server:default:App', + { variables: defaultVariables }); + expect(app.start).toHaveBeenCalledTimes(0); + }); + }); + describe('run', (): void => { it('starts the server with provided settings.', async(): Promise => { const parameters = { From 3a351c71b22abfdaf403de04d2f781d22318919f Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 16 Dec 2021 10:28:41 +0100 Subject: [PATCH 18/18] docs: Update comments in CLI-related classes --- src/init/AppRunner.ts | 14 +++++++--- src/init/cli/YargsCliExtractor.ts | 26 ++++++++++++++----- src/init/variables/computers/ArgExtractor.ts | 3 ++- .../variables/computers/AssetPathResolver.ts | 4 +-- .../variables/computers/BaseUrlComputer.ts | 3 ++- src/init/variables/computers/ValueComputer.ts | 2 +- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index bb3e9b92c6..0f25197b79 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -87,7 +87,7 @@ export class AppRunner { .options(baseArgs) .parseSync(); - // Gather settings for instantiating VarResolver + // Minimal settings that are required to create the initial ComponentsManager const loaderProperties = { mainModulePath: resolveAssetPath(params.mainModulePath), dumpErrorState: true, @@ -116,19 +116,27 @@ export class AppRunner { process.exit(1); } + /** + * Handles the first Components.js instantiation, + * where CLI settings and variable mappings are created. + */ private async resolveVariables(componentsManager: ComponentsManager, argv: string[]): Promise> { - // Create a resolver, that resolves componentsjs variables from cli-params + // Create a resolver, which combines a CliExtractor and a VariableResolver const resolver = await componentsManager.instantiate(DEFAULT_CLI_RESOLVER, {}); // Extract values from CLI parameters if (!resolver.extractor) { throw new Error('No CliExtractor is defined.'); } const cliValues = await resolver.extractor.handleSafe(argv); - // Using varResolver resolve variables + // Convert CLI values into variable mappings return await resolver.resolver.handleSafe(cliValues); } + /** + * The second Components.js instantiation, + * where the App is created and started using the variable mappings. + */ private async startApp(componentsManager: ComponentsManager, variables: Record): Promise { // Create app const app = await componentsManager.instantiate(DEFAULT_APP, { variables }); diff --git a/src/init/cli/YargsCliExtractor.ts b/src/init/cli/YargsCliExtractor.ts index 228500ae4d..11f095c39a 100644 --- a/src/init/cli/YargsCliExtractor.ts +++ b/src/init/cli/YargsCliExtractor.ts @@ -6,25 +6,31 @@ import { CliExtractor } from './CliExtractor'; export type YargsArgOptions = Record; export interface CliOptions { - // Usage string to be given at cli + // Usage string printed in case of CLI errors usage?: string; - // StrictMode determines wether to allow undefined cli-args or not. + // Errors on unknown CLI parameters when enabled. + // @see https://yargs.js.org/docs/#api-reference-strictenabledtrue strictMode?: boolean; - // Wether to load arguments from env-vars or not. + // Loads CLI args from environment variables when enabled. // @see http://yargs.js.org/docs/#api-reference-envprefix loadFromEnv?: boolean; - // Prefix for env-vars. - // see yargv docs for behavior. http://yargs.js.org/docs/#api-reference-envprefix + // Prefix to be used when `loadFromEnv` is enabled. + // @see http://yargs.js.org/docs/#api-reference-envprefix envVarPrefix?: string; } +/** + * Parses CLI args using the yargs library. + * Specific settings can be enabled through the provided options. + */ export class YargsCliExtractor extends CliExtractor { protected readonly yargsArgOptions: YargsArgOptions; protected readonly yargvOptions: CliOptions; /** - * @param parameters - record of option to it's yargs opt config. @range {json} - * @param options - options to configure yargv. @range {json} + * @param parameters - Parameters that should be parsed from the CLI. @range {json} + * Format details can be found at https://yargs.js.org/docs/#api-reference-optionskey-opt + * @param options - Additional options to configure yargs. @range {json} */ public constructor(parameters: YargsArgOptions = {}, options: CliOptions = {}) { super(); @@ -39,6 +45,9 @@ export class YargsCliExtractor extends CliExtractor { return yArgv.parse(); } + /** + * Creates the yargs Argv object based on the input CLI argv. + */ private createYArgv(argv: readonly string[]): Argv { let yArgv = yargs(argv.slice(2)); if (this.yargvOptions.usage !== undefined) { @@ -53,6 +62,9 @@ export class YargsCliExtractor extends CliExtractor { return yArgv.options(this.yargsArgOptions); } + /** + * Makes sure there are no positional arguments or multiple values for the same key. + */ private validateArguments(yArgv: Argv): Argv { return yArgv.check((args): boolean => { if (args._.length > 0) { diff --git a/src/init/variables/computers/ArgExtractor.ts b/src/init/variables/computers/ArgExtractor.ts index ac8573b278..910810f675 100644 --- a/src/init/variables/computers/ArgExtractor.ts +++ b/src/init/variables/computers/ArgExtractor.ts @@ -1,7 +1,8 @@ import { ValueComputer } from './ValueComputer'; /** - * Simple VarComputer that just extracts an arg from parsed args. + * A simple {@link ValueComputer} that extracts a single value from the input map. + * Returns the default value if it was defined in case no value was found in the map. */ export class ArgExtractor extends ValueComputer { private readonly key: string; diff --git a/src/init/variables/computers/AssetPathResolver.ts b/src/init/variables/computers/AssetPathResolver.ts index 7f2b508675..c2052882f9 100644 --- a/src/init/variables/computers/AssetPathResolver.ts +++ b/src/init/variables/computers/AssetPathResolver.ts @@ -2,8 +2,8 @@ import { resolveAssetPath } from '../../../util/PathUtil'; import { ValueComputer } from './ValueComputer'; /** - * This `VarComputer` resolves absolute path of asset, from path specified in specified argument. - * It follows conventions of `resolveAssetPath` function for path resolution. + * A {@link ValueComputer} that converts a path value to an absolute asset path by making use of `resolveAssetPath`. + * Returns the default path in case it is defined and no path was found in the map. */ export class AssetPathResolver extends ValueComputer { private readonly key: string; diff --git a/src/init/variables/computers/BaseUrlComputer.ts b/src/init/variables/computers/BaseUrlComputer.ts index f4e7581813..4852cb565a 100644 --- a/src/init/variables/computers/BaseUrlComputer.ts +++ b/src/init/variables/computers/BaseUrlComputer.ts @@ -2,7 +2,8 @@ import { ensureTrailingSlash } from '../../../util/PathUtil'; import { ValueComputer } from './ValueComputer'; /** - * A handler to compute base-url from args + * A {@link ValueComputer} that that generates the base URL based on the input `baseUrl` value, + * or by using the port if the first isn't provided. */ export class BaseUrlComputer extends ValueComputer { private readonly defaultPort: number; diff --git a/src/init/variables/computers/ValueComputer.ts b/src/init/variables/computers/ValueComputer.ts index 8c81657e2c..92154188e6 100644 --- a/src/init/variables/computers/ValueComputer.ts +++ b/src/init/variables/computers/ValueComputer.ts @@ -1,6 +1,6 @@ import { AsyncHandler } from '../../../util/handlers/AsyncHandler'; /** - A handler that determines the value of a specific variable from parsed cli arguments. + * A handler that computes a specific value from a given map of values. */ export abstract class ValueComputer extends AsyncHandler, unknown> {}