feat: allow css configuration from package.json or config file

This commit is contained in:
Jesse Wright 2022-11-23 21:26:31 +11:00 committed by Joachim Van Herwegen
parent bf0e35be37
commit d61bf9bf19
5 changed files with 271 additions and 10 deletions

View File

@ -22,6 +22,7 @@
- Regex-based configurations now have ordered entries and use the first match found.
- When starting the server through code, it is now possible to provide CLI value bindings as well in `AppRunner`.
- Support for Node v12 was dropped.
- The server configuration settings can be set from the package.json or .community-solid-server.config.json/.js files.
### Data migration

View File

@ -34,6 +34,7 @@ the [changelog](https://github.com/CommunitySolidServer/CommunitySolidServer/blo
* [How to use the Identity Provider](usage/identity-provider.md)
* [How to automate authentication](usage/client-credentials.md)
* [How to automatically seed pods on startup](usage/seeding-pods.md)
* [Using the CSS as a development server in another project](usage/dev-configuration.md)
## What the internals look like

View File

@ -0,0 +1,49 @@
# Configuring the CSS as a development server in another project
It can be useful to use the CSS as local server to develop Solid applications against.
As an alternative to using CLI arguments, or environment variables, the CSS can be configured in the `package.json` as follows:
```json
{
"name": "test",
"version": "0.0.0",
"private": "true",
"config": {
"community-solid-server": {
"port": 3001,
"loggingLevel": "error"
}
},
"scripts": {
"dev:pod": "community-solid-server"
},
"devDependencies": {
"@solid/community-server": "^6.0.0"
}
}
```
These parameters will then be used when the `community-solid-server`
command is executed as an npm script (as shown in the example above).
Or whenever the `community-solid-server` command is executed in the same
folder as the `package.json`.
Alternatively, the configuration parameters may be placed in a configuration file named
`.community-solid-server.config.json` as follows:
```json
{
"port": 3001,
"loggingLevel": "error"
}
```
The config may also be written in JavaScript with the config as the default export
such as the following `.community-solid-server.config.js`:
```js
module.exports = {
port: 3001,
loggingLevel: "error"
};
```

View File

@ -1,13 +1,15 @@
/* eslint-disable unicorn/no-process-exit */
import { existsSync } from 'fs';
import type { WriteStream } from 'tty';
import type { IComponentsManagerBuilderOptions } from 'componentsjs';
import { ComponentsManager } from 'componentsjs';
import { readJSON } from 'fs-extra';
import yargs from 'yargs';
import { LOG_LEVELS } from '../logging/LogLevel';
import { getLoggerFor } from '../logging/LogUtil';
import { createErrorMessage, isError } from '../util/errors/ErrorUtil';
import { InternalServerError } from '../util/errors/InternalServerError';
import { resolveModulePath, resolveAssetPath } from '../util/PathUtil';
import { resolveModulePath, resolveAssetPath, joinFilePath } from '../util/PathUtil';
import type { App } from './App';
import type { CliExtractor } from './cli/CliExtractor';
import type { CliResolver } from './CliResolver';
@ -135,7 +137,7 @@ export class AppRunner {
*/
public async createCli(argv: CliArgv = process.argv): Promise<App> {
// Parse only the core CLI arguments needed to load the configuration
const yargv = yargs(argv.slice(2))
let yargv = yargs(argv.slice(2))
.usage('node ./bin/server.js [args]')
.options(CORE_CLI_PARAMETERS)
// We disable help here as it would only show the core parameters
@ -143,6 +145,12 @@ export class AppRunner {
// We also read from environment variables
.env(ENV_VAR_PREFIX);
const settings = await this.getPackageSettings();
if (typeof settings !== 'undefined') {
yargv = yargv.default<object>(settings);
}
const params = await yargv.parse();
const loaderProperties = {
@ -165,12 +173,45 @@ export class AppRunner {
}
// Build the CLI components and use them to generate values for the Components.js variables
const variables = await this.cliToVariables(componentsManager, argv);
const variables = await this.cliToVariables(componentsManager, argv, settings);
// Build and start the actual server application using the generated variable values
return await this.createApp(componentsManager, variables);
}
/**
* Retrieves settings from package.json or configuration file when
* part of an npm project.
* @returns The settings defined in the configuration file
*/
public async getPackageSettings(): Promise<undefined | Record<string, unknown>> {
// Only try and retrieve config file settings if there is a package.json in the
// scope of the current directory
const packageJsonPath = joinFilePath(process.cwd(), 'package.json');
if (!existsSync(packageJsonPath)) {
return;
}
// First see if there is a dedicated .json configuration file
const cssConfigPath = joinFilePath(process.cwd(), '.community-solid-server.config.json');
if (existsSync(cssConfigPath)) {
return readJSON(cssConfigPath);
}
// Next see if there is a dedicated .js file
const cssConfigPathJs = joinFilePath(process.cwd(), '.community-solid-server.config.js');
if (existsSync(cssConfigPathJs)) {
return import(cssConfigPathJs);
}
// Finally try and read from the config.community-solid-server
// field in the root package.json
const pkg = await readJSON(packageJsonPath);
if (typeof pkg.config?.['community-solid-server'] === 'object') {
return pkg.config['community-solid-server'];
}
}
/**
* Creates the Components Manager that will be used for instantiating.
*/
@ -189,11 +230,14 @@ export class AppRunner {
* Handles the first Components.js instantiation.
* Uses it to extract the CLI shorthand values and use those to create variable bindings.
*/
private async cliToVariables(componentsManager: ComponentsManager<CliResolver>, argv: CliArgv):
Promise<VariableBindings> {
private async cliToVariables(
componentsManager: ComponentsManager<CliResolver>,
argv: CliArgv,
settings?: Record<string, unknown>,
): Promise<VariableBindings> {
const cliResolver = await this.createCliResolver(componentsManager);
const shorthand = await this.extractShorthand(cliResolver.cliExtractor, argv);
return await this.resolveShorthand(cliResolver.shorthandResolver, shorthand);
return await this.resolveShorthand(cliResolver.shorthandResolver, { ...settings, ...shorthand });
}
/**

View File

@ -7,20 +7,42 @@ import type { ShorthandResolver } from '../../../src/init/variables/ShorthandRes
import { joinFilePath } from '../../../src/util/PathUtil';
import { flushPromises } from '../../util/Util';
const defaultParameters = {
let defaultParameters: Record<string, any> = {
port: 3000,
logLevel: 'info',
};
const cliExtractor: jest.Mocked<CliExtractor> = {
handleSafe: jest.fn().mockResolvedValue(defaultParameters),
handleSafe: jest.fn((): Record<string, any> => defaultParameters),
} as any;
const defaultVariables = {
let defaultVariables: Record<string, any> = {
'urn:solid-server:default:variable:port': 3000,
'urn:solid-server:default:variable:loggingLevel': 'info',
};
const shorthandKeys: Record<string, string> = {
port: 'urn:solid-server:default:variable:port',
logLevel: 'urn:solid-server:default:variable:loggingLevel',
};
const shorthandResolver: jest.Mocked<ShorthandResolver> = {
handleSafe: jest.fn().mockResolvedValue(defaultVariables),
handleSafe: jest.fn((args: Record<string, any>): Record<string, any> => {
const variables: Record<string, any> = {};
for (const key in args) {
if (key in shorthandKeys) {
variables[shorthandKeys[key]] = args[key];
// We ignore the default key as this is introduced by the way
// we are mocking the module
} else if (key !== 'default') {
throw new Error(`Unexpected key ${key}`);
}
}
return variables;
}),
} as any;
const mockLogger = {
@ -74,11 +96,61 @@ jest.mock('componentsjs', (): any => ({
},
}));
let files: Record<string, any> = {};
const alternateParameters = {
port: 3101,
logLevel: 'error',
};
const packageJSONbase = {
name: 'test',
version: '0.0.0',
private: true,
};
const packageJSON = {
...packageJSONbase,
config: {
'community-solid-server': alternateParameters,
},
};
jest.mock('fs', (): Partial<Record<string, jest.Mock>> => ({
cwd: jest.fn((): string => __dirname),
existsSync: jest.fn((pth: string): boolean => typeof pth === 'string' && pth in files),
}));
jest.mock('fs-extra', (): Partial<Record<string, jest.Mock>> => ({
readJSON: jest.fn(async(pth: string): Promise<any> => files[pth]),
pathExists: jest.fn(async(pth: string): Promise<boolean> => typeof pth === 'string' && pth in files),
}));
jest.mock(
'/var/cwd/.community-solid-server.config.js',
(): any => alternateParameters,
{ virtual: true },
);
jest.spyOn(process, 'cwd').mockReturnValue('/var/cwd');
const write = jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn());
const exit = jest.spyOn(process, 'exit').mockImplementation(jest.fn() as any);
describe('AppRunner', (): void => {
beforeEach((): void => {
files = {};
defaultParameters = {
port: 3000,
logLevel: 'info',
};
defaultVariables = {
'urn:solid-server:default:variable:port': 3000,
'urn:solid-server:default:variable:loggingLevel': 'info',
};
});
afterEach((): void => {
jest.clearAllMocks();
});
@ -547,6 +619,100 @@ describe('AppRunner', (): void => {
}
});
it('runs with no parameters.', async(): Promise<void> => {
defaultParameters = {};
defaultVariables = {};
await expect(new AppRunner().runCli()).resolves.toBeUndefined();
expect(manager.instantiate).toHaveBeenNthCalledWith(
2, 'urn:solid-server:default:App', { variables: {}},
);
});
it('runs honouring package.json configuration.', async(): Promise<void> => {
files = { '/var/cwd/package.json': packageJSON };
defaultParameters = {};
defaultVariables = {};
await expect(new AppRunner().runCli()).resolves.toBeUndefined();
expect(manager.instantiate).toHaveBeenNthCalledWith(
2, 'urn:solid-server:default:App', { variables: {
'urn:solid-server:default:variable:port': 3101,
'urn:solid-server:default:variable:loggingLevel': 'error',
}},
);
});
it('runs honouring package.json configuration with empty config.', async(): Promise<void> => {
files = { '/var/cwd/package.json': packageJSONbase };
defaultParameters = {};
defaultVariables = {};
await expect(new AppRunner().runCli()).resolves.toBeUndefined();
expect(manager.instantiate).toHaveBeenNthCalledWith(
2, 'urn:solid-server:default:App', { variables: {}},
);
});
it('runs honouring .community-solid-server.config.json if package.json is present.', async(): Promise<void> => {
files = {
'/var/cwd/.community-solid-server.config.json': alternateParameters,
'/var/cwd/package.json': packageJSONbase,
};
defaultParameters = {};
defaultVariables = {};
await expect(new AppRunner().runCli()).resolves.toBeUndefined();
expect(manager.instantiate).toHaveBeenNthCalledWith(
2, 'urn:solid-server:default:App', { variables: {
'urn:solid-server:default:variable:port': 3101,
'urn:solid-server:default:variable:loggingLevel': 'error',
}},
);
});
it('runs honouring .community-solid-server.config.js if package.json is present.', async(): Promise<void> => {
files = {
'/var/cwd/.community-solid-server.config.js': alternateParameters,
'/var/cwd/package.json': packageJSONbase,
};
defaultParameters = {};
defaultVariables = {};
await expect(new AppRunner().runCli()).resolves.toBeUndefined();
expect(manager.instantiate).toHaveBeenNthCalledWith(
2, 'urn:solid-server:default:App', { variables: {
'urn:solid-server:default:variable:port': 3101,
'urn:solid-server:default:variable:loggingLevel': 'error',
}},
);
});
it('runs ignoring .community-solid-server.config.json if no package.json present.', async(): Promise<void> => {
files = { '/var/cwd/.community-solid-server.config.json': alternateParameters };
defaultParameters = {};
defaultVariables = {};
await expect(new AppRunner().runCli()).resolves.toBeUndefined();
expect(manager.instantiate).toHaveBeenNthCalledWith(
2, 'urn:solid-server:default:App', { variables: {}},
);
});
it('runs ignoring .community-solid-server.config.js if no package.json present.', async(): Promise<void> => {
files = {
'/var/cwd/.community-solid-server.config.js': `module.exports = ${JSON.stringify(alternateParameters)}`,
};
defaultParameters = {};
defaultVariables = {};
await expect(new AppRunner().runCli()).resolves.toBeUndefined();
expect(manager.instantiate).toHaveBeenNthCalledWith(
2, 'urn:solid-server:default:App', { variables: {}},
);
});
it('throws an error if the server could not start.', async(): Promise<void> => {
app.start.mockRejectedValueOnce(new Error('Fatal'));