From e050f8be93b7ea9596fdaf250de9f0bf32fa4fd8 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 23 Aug 2022 16:37:07 +0200 Subject: [PATCH] feat: Allow multiple configurations to be used during startup --- README.md | 26 +++++++++++++------------- config/app/variables/cli/cli.json | 4 ++-- src/init/AppRunner.ts | 22 ++++++++++++---------- test/integration/Cli.test.ts | 2 +- test/unit/init/AppRunner.test.ts | 31 +++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c9b2db73b..3027b684c 100644 --- a/README.md +++ b/README.md @@ -122,19 +122,19 @@ by passing parameters to the server command. These parameters give you direct access to some commonly used settings: -| parameter name | default value | description | -|------------------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| `--port, -p` | `3000` | The TCP port on which the server should listen. | -| `--baseUrl, -b` | `http://localhost:$PORT/` | The base URL used internally to generate URLs. Change this if your server does not run on `http://localhost:$PORT/`. | -| `--loggingLevel, -l` | `info` | The detail level of logging; useful for debugging problems. Use `debug` for full information. | -| `--config, -c` | `@css:config/default.json` | The configuration for the server. The default only stores data in memory; to persist to your filesystem, use `@css:config/file.json` | -| `--rootFilePath, -f` | `./` | Root folder where the server stores data, when using a file-based configuration. | -| `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. | -| `--showStackTrace, -t` | false | Enables detailed logging on error output. | -| `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. Only relevant when using `@css:config/dynamic.json`. | -| `--seededPodConfigJson`| | Path to the file that keeps track of seeded Pod configurations. | -| `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations. | -| `--workers, -w` | `1` | Run in multithreaded mode using workers. Special values are `-1` (scale to `num_cores-1`), `0` (scale to `num_cores`) and 1 (singlethreaded). | +| parameter name | default value | description | +|------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `--port, -p` | `3000` | The TCP port on which the server should listen. | +| `--baseUrl, -b` | `http://localhost:$PORT/` | The base URL used internally to generate URLs. Change this if your server does not run on `http://localhost:$PORT/`. | +| `--loggingLevel, -l` | `info` | The detail level of logging; useful for debugging problems. Use `debug` for full information. | +| `--config, -c` | `@css:config/default.json` | The configuration(s) for the server. The default only stores data in memory; to persist to your filesystem, use `@css:config/file.json` | +| `--rootFilePath, -f` | `./` | Root folder where the server stores data, when using a file-based configuration. | +| `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. | +| `--showStackTrace, -t` | false | Enables detailed logging on error output. | +| `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. Only relevant when using `@css:config/dynamic.json`. | +| `--seededPodConfigJson`| | Path to the file that keeps track of seeded Pod configurations. | +| `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations. | +| `--workers, -w` | `1` | Run in multithreaded mode using workers. Special values are `-1` (scale to `num_cores-1`), `0` (scale to `num_cores`) and 1 (singlethreaded). | ### 🔀 Multithreading diff --git a/config/app/variables/cli/cli.json b/config/app/variables/cli/cli.json index b9eeaf73c..bbeb70111 100644 --- a/config/app/variables/cli/cli.json +++ b/config/app/variables/cli/cli.json @@ -12,8 +12,8 @@ "options": { "alias": "c", "requiresArg": true, - "type": "string", - "describe": "The configuration for the server. The default only stores data in memory; to persist to your filesystem, use @css:config/file.json." + "type": "array", + "describe": "The configuration(s) for the server. The default only stores data in memory; to persist to your filesystem, use @css:config/file.json." } }, { diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index 09e8126ca..32cd1237d 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -21,7 +21,7 @@ const DEFAULT_CLI_RESOLVER = 'urn:solid-server-app-setup:default:CliResolver'; const DEFAULT_APP = 'urn:solid-server:default:App'; const CORE_CLI_PARAMETERS = { - config: { type: 'string', alias: 'c', default: DEFAULT_CONFIG, requiresArg: true }, + config: { type: 'array', alias: 'c', default: [ DEFAULT_CONFIG ], requiresArg: true }, loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS }, mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, } as const; @@ -48,13 +48,13 @@ export class AppRunner { * The values in `variableBindings` take priority over those in `shorthand`. * * @param loaderProperties - Components.js loader properties. - * @param configFile - Path to the server config file. + * @param configFile - Path to the server config file(s). * @param variableBindings - Bindings of Components.js variables. * @param shorthand - Shorthand values that need to be resolved. */ public async run( loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, + configFile: string | string[], variableBindings?: VariableBindings, shorthand?: Shorthand, ): Promise { @@ -75,13 +75,13 @@ export class AppRunner { * The values in `variableBindings` take priority over those in `shorthand`. * * @param loaderProperties - Components.js loader properties. - * @param configFile - Path to the server config file. + * @param configFile - Path to the server config file(s). * @param variableBindings - Bindings of Components.js variables. * @param shorthand - Shorthand values that need to be resolved. */ public async create( loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, + configFile: string | string[], variableBindings?: VariableBindings, shorthand?: Shorthand, ): Promise { @@ -152,16 +152,16 @@ export class AppRunner { typeChecking: false, }; - const config = resolveAssetPath(params.config); + const configs = params.config.map(resolveAssetPath); // Create the Components.js manager used to build components from the provided config let componentsManager: ComponentsManager; try { - componentsManager = await this.createComponentsManager(loaderProperties, config); + componentsManager = await this.createComponentsManager(loaderProperties, configs); } catch (error: unknown) { // Print help of the expected core CLI parameters const help = await yargv.getHelp(); - this.resolveError(`${help}\n\nCould not build the config files from ${config}`, error); + this.resolveError(`${help}\n\nCould not build the config files from ${configs}`, error); } // Build the CLI components and use them to generate values for the Components.js variables @@ -176,10 +176,12 @@ export class AppRunner { */ public async createComponentsManager( loaderProperties: IComponentsManagerBuilderOptions, - configFile: string, + configFile: string | string[], ): Promise> { const componentsManager = await ComponentsManager.build(loaderProperties); - await componentsManager.configRegistry.register(configFile); + for (const config of Array.isArray(configFile) ? configFile : [ configFile ]) { + await componentsManager.configRegistry.register(config); + } return componentsManager; } diff --git a/test/integration/Cli.test.ts b/test/integration/Cli.test.ts index ff7fd6252..8c9f94c71 100644 --- a/test/integration/Cli.test.ts +++ b/test/integration/Cli.test.ts @@ -29,7 +29,7 @@ describe('An instantiated CliResolver', (): void => { '-s', 's', '-w', '2', ]); - expect(shorthand.config).toBe('c'); + expect(shorthand.config).toEqual([ 'c' ]); expect(shorthand.mainModulePath).toBe('m'); expect(shorthand.loggingLevel).toBe('l'); expect(shorthand.baseUrl).toBe('b'); diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index e975f9747..77e90f8c6 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -281,6 +281,37 @@ describe('AppRunner', (): void => { expect(app.start).toHaveBeenCalledTimes(0); }); + it('can apply multiple configurations.', async(): Promise => { + const params = [ + 'node', 'script', + '-c', 'config1.json', 'config2.json', + ]; + await expect(new AppRunner().createCli(params)).resolves.toBe(app); + + expect(ComponentsManager.build).toHaveBeenCalledTimes(1); + expect(ComponentsManager.build).toHaveBeenCalledWith({ + dumpErrorState: true, + logLevel: 'info', + mainModulePath: joinFilePath(__dirname, '../../../'), + typeChecking: false, + }); + expect(manager.configRegistry.register).toHaveBeenCalledTimes(2); + expect(manager.configRegistry.register).toHaveBeenNthCalledWith(1, '/var/cwd/config1.json'); + expect(manager.configRegistry.register).toHaveBeenNthCalledWith(2, '/var/cwd/config2.json'); + expect(manager.instantiate).toHaveBeenCalledTimes(2); + expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {}); + expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(1); + expect(cliExtractor.handleSafe).toHaveBeenCalledWith(params); + expect(shorthandResolver.handleSafe).toHaveBeenCalledTimes(1); + expect(shorthandResolver.handleSafe).toHaveBeenCalledWith(defaultParameters); + expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {}); + expect(manager.instantiate).toHaveBeenNthCalledWith(2, + 'urn:solid-server:default:App', + { variables: defaultVariables }); + expect(app.clusterManager.isSingleThreaded()).toBeFalsy(); + expect(app.start).toHaveBeenCalledTimes(0); + }); + it('uses the default process.argv in case none are provided.', async(): Promise => { const { argv } = process; const argvParameters = [