Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat [semver.minor]: allow css configuration from package.json #1499

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b7ef959
feat: allow css configuration from package.json
jeswr Nov 4, 2022
47a4af4
chore: fix lint errors
jeswr Nov 4, 2022
30636b4
fix: pass shorthand parameters correctly
jeswr Nov 4, 2022
3cb6212
chore: allow css config to be set with .community-solid-server.config…
jeswr Nov 4, 2022
eeb93e9
chore: add tests for package.json invocation
jeswr Nov 4, 2022
633d95a
chore: remove unecessary deps in test packages
jeswr Nov 4, 2022
d1d67e9
chore: use path constants
jeswr Nov 4, 2022
f875b62
chore: add documentation for initialisation from package.json
jeswr Nov 4, 2022
5855d1e
chore: add documentation for initialisation from package.json
jeswr Nov 4, 2022
ea40c66
chore: fix AppRunner paths
jeswr Nov 5, 2022
0de2855
chore: fix test mocking
jeswr Nov 5, 2022
9b8898c
chore: run lint:fix
jeswr Nov 5, 2022
e0091e9
chore: remove uncessary comment
jeswr Nov 5, 2022
f2065c4
feat: allow config to be js
jeswr Nov 5, 2022
b8c6e75
chore: document js configuration
jeswr Nov 5, 2022
c26f59d
chore: run lint:markdown:fix
jeswr Nov 19, 2022
d71227b
Apply documentation suggestions from code review
jeswr Nov 21, 2022
5ba4967
chore: fix lint errors
jeswr Nov 21, 2022
239e9e3
chore: specify that the package.json is an alternative to CLI or env …
jeswr Nov 21, 2022
966d4c6
fix: use joinFilePath instead of path.join
jeswr Nov 22, 2022
c0e25aa
fix:yargv type casting
jeswr Nov 22, 2022
db76bb6
chore: fix yarv type casting
jeswr Nov 22, 2022
64eda7e
chore: specify how css paramaters are used
jeswr Nov 22, 2022
04986ab
chore: reoder using server commands
jeswr Nov 22, 2022
1ce7da9
chore: revert .eslintrc.js
jeswr Nov 22, 2022
f62fa66
chore: move package init tests to AppRunner.test.ts
jeswr Nov 22, 2022
10d6b8e
chore: move community-solid-server.config.js mock to global scope
jeswr Nov 22, 2022
6ce9674
Update RELEASE_NOTES.md
jeswr Nov 23, 2022
f16e4ba
Update documentation/markdown/usage/dev-configuration.md
jeswr Nov 23, 2022
6192086
Update src/init/AppRunner.ts
jeswr Nov 23, 2022
9f48c54
Update src/init/AppRunner.ts
jeswr Nov 23, 2022
03253d4
Update src/init/AppRunner.ts
jeswr Nov 23, 2022
2b18b87
Update src/init/AppRunner.ts
jeswr Nov 23, 2022
58ba248
chore: move getPackageSettings and remove unecessary return
jeswr Nov 23, 2022
a3fbeda
chore: run lint:ts -- --fix
jeswr Nov 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Support for the new [WebSocket Notification protocol](https://solidproject.org/TR/websocket-subscription-2021)
and the [WebHook Notification protocol draft](https://github.com/solid/notifications/blob/main/webhook-subscription-2021.md)
was added.
- The server configuration settings can be set from the package.json or .community-solid-server.config.json/.js files.

### Data migration

Expand Down
1 change: 1 addition & 0 deletions documentation/markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 49 additions & 0 deletions documentation/markdown/usage/dev-configuration.md
Original file line number Diff line number Diff line change
@@ -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"
};
```
56 changes: 50 additions & 6 deletions src/init/AppRunner.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -135,14 +137,20 @@ 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
.help(false)
// 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 = {
Expand All @@ -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.
*/
Expand All @@ -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 });
}

/**
Expand Down
174 changes: 170 additions & 4 deletions test/unit/init/AppRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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'));

Expand Down