Merge branch 'main' into versions/6.0.0

# Conflicts:
#	src/init/ServerInitializer.ts
#	src/server/BaseHttpServerFactory.ts
#	src/server/HttpServerFactory.ts
#	src/server/WebSocketServerFactory.ts
#	test/unit/server/BaseHttpServerFactory.test.ts
This commit is contained in:
Joachim Van Herwegen 2023-02-01 10:13:04 +01:00
commit 7cc0e3fbcc
30 changed files with 972 additions and 653 deletions

View File

@ -42,7 +42,7 @@ jobs:
with:
node-version: 16.x
- name: Check out the project
uses: actions/checkout@v3.1.0
uses: actions/checkout@v3.3.0
with:
ref: ${{ inputs.branch || github.ref }}
- name: Install dependencies and run build scripts

View File

@ -21,7 +21,7 @@ jobs:
tags: ${{ steps.meta-main.outputs.tags || steps.meta-version.outputs.tags }}
steps:
- name: Checkout
uses: actions/checkout@v3.1.0
uses: actions/checkout@v3.3.0
- if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
name: Docker meta edge and version tag
id: meta-main
@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3.1.0
uses: actions/checkout@v3.3.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
@ -66,7 +66,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and export to docker
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
load: true
@ -85,10 +85,10 @@ jobs:
done <<< "${{ needs.docker-meta.outputs.tags }}";
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v7
platforms: linux/amd64,linux/arm/v7,linux/arm/v8
tags: ${{ needs.docker-meta.outputs.tags }}
labels: ${{ needs.docker-meta.outputs.labels }}

View File

@ -34,12 +34,7 @@ jobs:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
typedocs-release:
# Release typedocs on version tag, but ignore pre-releases
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
uses: ./.github/workflows/typedocs.yml
mkdocs-release:
# Release mkdocs on version tag, but ignore pre-releases
# Release documentation on version tag, but ignore pre-releases
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-')
uses: ./.github/workflows/mkdocs.yml

View File

@ -14,14 +14,14 @@ on:
jobs:
mkdocs-prep:
# Runs the markdownlinter to ensure we don't release faulty markdown.
# Also gets the correct major version, wether the job is triggered by a version tag
# Runs the markdown linter to ensure we don't release faulty markdown.
# Also gets the correct major version, whether the job is triggered by a version tag
# or a push to main to update the latest documentation.
runs-on: ubuntu-latest
outputs:
major: ${{ steps.tagged_version.outputs.major || steps.current_version.ouputs.major }}
major: ${{ steps.tagged_version.outputs.major || steps.current_version.outputs.major }}
steps:
- uses: actions/checkout@v3.1.0
- uses: actions/checkout@v3.3.0
- uses: actions/setup-node@v3
with:
node-version: '16.x'
@ -37,13 +37,13 @@ jobs:
id: current_version
run: |
VERSION=$(git show origin/main:package.json | jq -r .version | grep -Po '^(\d+)')
echo "::set-output name=major::$VERSION"
echo "major=$VERSION" >> $GITHUB_OUTPUT
mkdocs:
runs-on: ubuntu-latest
needs: mkdocs-prep
steps:
- uses: actions/checkout@v3.1.0
- uses: actions/checkout@v3.3.0
- uses: actions/setup-python@v4
with:
python-version: 3.x
@ -54,4 +54,25 @@ jobs:
- run: git fetch origin gh-pages --depth=1
- run: |
cd documentation && mike deploy --push --update-aliases \
${{ needs.mkdocs-prep.outputs.major}}.x latest
${{ needs.mkdocs-prep.outputs.major }}.x latest
typedocs:
# Build typedocs and publish them to the GH page.
# `mike deploy` overwrites the entire folder for a version so these need to be (re)built afterwards.
needs: [mkdocs-prep, mkdocs]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.3.0
- uses: actions/setup-node@v3
with:
node-version: '16.x'
- run: npm ci --ignore-scripts
- name: Generate typedocs
run: npm run typedocs
- name: Deploy typedocs
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
destination_dir: ${{ needs.mkdocs-prep.outputs.major }}.x/docs

View File

@ -7,7 +7,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.1.0
- uses: actions/checkout@v3.3.0
- uses: actions/setup-node@v3
with:
node-version: '16.x'
@ -38,7 +38,7 @@ jobs:
- name: Ensure line endings are consistent
run: git config --global core.autocrlf input
- name: Check out repository
uses: actions/checkout@v3.1.0
uses: actions/checkout@v3.3.0
- name: Install dependencies and run build scripts
run: npm ci
- name: Type-check tests
@ -81,7 +81,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Check out repository
uses: actions/checkout@v3.1.0
uses: actions/checkout@v3.3.0
- name: Install dependencies and run build scripts
run: npm ci
- name: Run integration tests
@ -105,7 +105,7 @@ jobs:
- name: Ensure line endings are consistent
run: git config --global core.autocrlf input
- name: Check out repository
uses: actions/checkout@v3.1.0
uses: actions/checkout@v3.3.0
- name: Install dependencies and run build scripts
run: npm ci
- name: Run integration tests
@ -127,7 +127,7 @@ jobs:
with:
node-version: '16.x'
- name: Check out repository
uses: actions/checkout@v3.1.0
uses: actions/checkout@v3.3.0
- name: Install dependencies and run build scripts
run: npm ci
- name: Run deploy tests

View File

@ -10,7 +10,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v6
- uses: actions/stale@v7
with:
debug-only: true
stale-issue-label: 🏚️ abandoned

View File

@ -1,25 +0,0 @@
name: Typedocs
on:
workflow_call:
jobs:
typedocs:
# Build typedocs and publish them to the GH page
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.1.0
- uses: actions/setup-node@v3
with:
node-version: '16.x'
- run: npm ci --ignore-scripts
- name: Generate typedocs
run: npm run typedocs
- name: Get tagged version
id: version
uses: battila7/get-version-action@v2
- name: Deploy typedocs
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
destination_dir: ${{ steps.version.outputs.major }}.x/docs

File diff suppressed because it is too large Load Diff

View File

@ -115,26 +115,27 @@ testing applications in different setups,
or developing new parts for the server
without needing to change its base code.
### ⏱ Parameters
### ⏱ Parameters
An easy way to customize the server is
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(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). |
| 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/`. |
| `--socket` | | The Unix Domain Socket on which the server should listen. `--baseUrl` must be set if this option is provided |
| `--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
@ -142,7 +143,7 @@ The Community Solid Server can be started in multithreaded mode with any config.
that are threadsafe though. If a non-threadsafe component is used in multithreaded mode, the server will describe with
an error which class is the culprit.
```node
```shell
# Running multithreaded with autoscaling to number of logical cores minus 1
npm start -- -c config/file.json -w -1
```
@ -174,13 +175,10 @@ Recipes for configuring the server can be found at [CommunitySolidServer/recipes
The server allows writing and plugging in custom modules
without altering its base source code.
The [📗 API documentation](https://communitysolidserver.github.io/CommunitySolidServer/latest/docs) and
the [📐 architectural diagram](https://rubenverborgh.github.io/solid-server-architecture/solid-architecture-v1-3-0.pdf)
The [📗 API documentation](https://communitysolidserver.github.io/CommunitySolidServer/latest/5.x/docs) and
the [📓 user documentation](https://communitysolidserver.github.io/CommunitySolidServer/)
can help you find your way.
If you want to help out with server development,
have a look at the [📓 user documentation](https://communitysolidserver.github.io/CommunitySolidServer/) and
[🛠 good first issues](https://github.com/CommunitySolidServer/CommunitySolidServer/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
There is also a repository of [📚 comprehensive tutorials](https://github.com/CommunitySolidServer/tutorials/)
## 📜 License
@ -189,13 +187,6 @@ is copyrighted by [Inrupt Inc.](https://inrupt.com/)
and [imec](https://www.imec-int.com/)
and available under the [MIT License](https://github.com/CommunitySolidServer/CommunitySolidServer/blob/main/LICENSE.md).
Core contributors are
[Joachim Van Herwegen](https://github.com/joachimvh),
[Ruben Verborgh](https://github.com/RubenVerborgh),
[Ruben Taelman](https://github.com/rubensworks),
and
[Matthieu Bosquet](https://github.com/matthieubosquet).
## 🎤 Feedback and questions
Don't hesitate to [start a discussion](https://github.com/CommunitySolidServer/CommunitySolidServer/discussions)

View File

@ -96,6 +96,7 @@ These changes are relevant if you wrote custom modules for the server that depen
- 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

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
To report and discuss security vulnerabilities go to <https://github.com/CommunitySolidServer/CommunitySolidServer/security/advisories>

View File

@ -1,3 +1,10 @@
#!/usr/bin/env node
const { AppRunner } = require('..');
// Attaching a logger to the uncaughtExceptionMonitor event,
// such that the default uncaughtException behavior still occurs.
process.on('uncaughtExceptionMonitor', (err, origin) => {
console.error(`Process is halting due to an ${origin} with error ${err.message}`);
});
new AppRunner().runCliSync(process);

View File

@ -6,7 +6,8 @@
"@id": "urn:solid-server:default:ServerInitializer",
"@type": "ServerInitializer",
"serverFactory": { "@id": "urn:solid-server:default:ServerFactory" },
"port": { "@id": "urn:solid-server:default:variable:port" }
"port": { "@id": "urn:solid-server:default:variable:port" },
"socketPath": { "@id": "urn:solid-server:default:variable:socket" }
}
]
}

View File

@ -56,6 +56,15 @@
"describe": "The TCP port on which the server runs."
}
},
{
"@type": "YargsParameter",
"name": "socket",
"options": {
"requiresArg": true,
"type": "string",
"describe": "The path to the Unix Domain Socket on which the server runs. This overrides the port argument."
}
},
{
"@type": "YargsParameter",
"name": "rootFilePath",

View File

@ -28,6 +28,14 @@
"defaultValue": 3000
}
},
{
"CombinedShorthandResolver:_resolvers_key": "urn:solid-server:default:variable:socket",
"CombinedShorthandResolver:_resolvers_value": {
"@type": "KeyExtractor",
"key": "socket",
"defaultValue" : ""
}
},
{
"CombinedShorthandResolver:_resolvers_key": "urn:solid-server:default:variable:rootFilePath",
"CombinedShorthandResolver:_resolvers_value": {

View File

@ -23,6 +23,7 @@
"openid": [ "azp" ],
"webid": [ "webid" ]
},
"clockTolerance": 120,
"cookies": {
"long": { "signed": true, "maxAge": 86400000 },
"short": { "signed": true }

View File

@ -7,6 +7,11 @@
"@id": "urn:solid-server:default:variable:port",
"@type": "Variable"
},
{
"comment": "Unix Domain Socket of the server.",
"@id": "urn:solid-server:default:variable:socket",
"@type": "Variable"
},
{
"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",

View File

@ -34,12 +34,17 @@ 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
* [How the server uses dependency injection](architecture/dependency-injection.md)
* [What the architecture looks like](architecture/overview.md)
## Comprehensive guides and tutorials
* [The CSS tutorial repository](https://github.com/CommunitySolidServer/tutorials/)
## Making changes
* [How to make changes to the repository](contributing/making-changes.md)

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"
};
```

122
package-lock.json generated
View File

@ -38,7 +38,7 @@
"arrayify-stream": "^2.0.0",
"async-lock": "^1.3.2",
"bcryptjs": "^2.4.3",
"componentsjs": "^5.3.0",
"componentsjs": "^5.3.2",
"cors": "^2.8.5",
"cross-fetch": "^3.1.5",
"ejs": "^3.1.8",
@ -6136,9 +6136,9 @@
"dev": true
},
"node_modules/componentsjs": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.3.0.tgz",
"integrity": "sha512-eteUmYCezs4a/ZBVxk3graGPlNNrdilSrmsWOSWOYO41KTqe054Q5zSdpetNNchhOUwPbIcdP29JuuBdAd2/fQ==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.3.2.tgz",
"integrity": "sha512-wqXaHjrnT4UDQT8Eaou/Itd55OWE7wasBivPJ0qfSlRMi5zRAwp3+sEgGO7F5T7hs0rMsrGTnkWWcoSHmrM/8A==",
"dependencies": {
"@rdfjs/types": "*",
"@types/minimist": "^1.2.0",
@ -6702,9 +6702,9 @@
"dev": true
},
"node_modules/cookiejar": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"dev": true
},
"node_modules/cookies": {
@ -7143,9 +7143,9 @@
}
},
"node_modules/dezalgo": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
"integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true,
"dependencies": {
"asap": "^2.0.0",
@ -8782,32 +8782,20 @@
}
},
"node_modules/formidable": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
"integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz",
"integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==",
"dev": true,
"dependencies": {
"dezalgo": "1.0.3",
"hexoid": "1.0.0",
"once": "1.4.0",
"qs": "6.9.3"
"dezalgo": "^1.0.4",
"hexoid": "^1.0.0",
"once": "^1.4.0",
"qs": "^6.11.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/formidable/node_modules/qs": {
"version": "6.9.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
"integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==",
"dev": true,
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -11077,9 +11065,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@ -13718,9 +13706,9 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/simple-git": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.12.0.tgz",
"integrity": "sha512-cy1RSRFHGZSrlYa3MnUuNVOXLUdifEZD2X8+AZjg8mKCdRvtCFSga6acq5N2g0ggb8lH3jBi369MrFZ+Y6sfsA==",
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz",
"integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==",
"dev": true,
"dependencies": {
"@kwsites/file-exists": "^1.1.1",
@ -14592,9 +14580,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@ -20402,9 +20390,9 @@
"dev": true
},
"componentsjs": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.3.0.tgz",
"integrity": "sha512-eteUmYCezs4a/ZBVxk3graGPlNNrdilSrmsWOSWOYO41KTqe054Q5zSdpetNNchhOUwPbIcdP29JuuBdAd2/fQ==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/componentsjs/-/componentsjs-5.3.2.tgz",
"integrity": "sha512-wqXaHjrnT4UDQT8Eaou/Itd55OWE7wasBivPJ0qfSlRMi5zRAwp3+sEgGO7F5T7hs0rMsrGTnkWWcoSHmrM/8A==",
"requires": {
"@rdfjs/types": "*",
"@types/minimist": "^1.2.0",
@ -20854,9 +20842,9 @@
}
},
"cookiejar": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"dev": true
},
"cookies": {
@ -21184,9 +21172,9 @@
"dev": true
},
"dezalgo": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
"integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true,
"requires": {
"asap": "^2.0.0",
@ -22426,23 +22414,15 @@
}
},
"formidable": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
"integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz",
"integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==",
"dev": true,
"requires": {
"dezalgo": "1.0.3",
"hexoid": "1.0.0",
"once": "1.4.0",
"qs": "6.9.3"
},
"dependencies": {
"qs": {
"version": "6.9.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
"integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==",
"dev": true
}
"dezalgo": "^1.0.4",
"hexoid": "^1.0.0",
"once": "^1.4.0",
"qs": "^6.11.0"
}
},
"fresh": {
@ -24134,9 +24114,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonc-parser": {
@ -26166,9 +26146,9 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"simple-git": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.12.0.tgz",
"integrity": "sha512-cy1RSRFHGZSrlYa3MnUuNVOXLUdifEZD2X8+AZjg8mKCdRvtCFSga6acq5N2g0ggb8lH3jBi369MrFZ+Y6sfsA==",
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz",
"integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==",
"dev": true,
"requires": {
"@kwsites/file-exists": "^1.1.1",
@ -26853,9 +26833,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"

View File

@ -83,7 +83,7 @@
"commit-and-tag-version": {
"scripts": {
"postbump": "ts-node ./scripts/upgradeConfig.ts",
"postchangelog": "ts-node ./scripts/formatChangelog.ts"
"postchangelog": "ts-node ./scripts/formatChangelog.ts && markdownlint-cli2-fix"
},
"writerOpts": {
"commitsSort": false
@ -128,7 +128,7 @@
"arrayify-stream": "^2.0.0",
"async-lock": "^1.3.2",
"bcryptjs": "^2.4.3",
"componentsjs": "^5.3.0",
"componentsjs": "^5.3.2",
"cors": "^2.8.5",
"cross-fetch": "^3.1.5",
"ejs": "^3.1.8",

View File

@ -1,4 +1,5 @@
#!/usr/bin/env ts-node
/* eslint-disable no-console */
import { readFile, writeFile } from 'fs-extra';
@ -8,18 +9,16 @@ import { readFile, writeFile } from 'fs-extra';
* to the changelog.
* Current automatic changes:
* - Change all version titles to H2 ("### [vX.Y.Z]" to "## [vX.Y.Z]")
* - Capitalize all list entries
*/
/**
* @param from - Regular expression to search for
* @param to - String to replace to
* @param filePath - File to search/replace
* @returns Promise
* Capitalize all list entries
* @param input - String to search/replace
* @returns Promise with output string
*/
async function replaceInFile(from: RegExp, to: string, filePath: string): Promise<void> {
const data = await readFile(filePath, 'utf8');
const result = data.replace(from, to);
return writeFile(filePath, result, 'utf8');
async function capitalizeListEntries(input: string): Promise<string> {
return input.replace(/^(\W*\* [a-z])/gmu, (match): string => match.toUpperCase());
}
/**
@ -30,4 +29,15 @@ function endProcess(error: Error): never {
process.exit(1);
}
replaceInFile(/### \[/gu, '## [', 'CHANGELOG.md').catch(endProcess);
/**
* Main function for changelog formatting
* @param filePath - Path to the changelog file
* @returns Promise
*/
async function formatChangelog(filePath: string): Promise<void> {
let changelog = await readFile(filePath, 'utf8');
changelog = await capitalizeListEntries(changelog);
return writeFile(filePath, changelog, 'utf8');
}
formatChangelog('CHANGELOG.md').catch(endProcess);

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

@ -14,22 +14,32 @@ export class ServerInitializer extends Initializer implements Finalizable {
protected readonly logger = getLoggerFor(this);
private readonly serverFactory: HttpServerFactory;
private readonly port: number;
private readonly port?: number;
private readonly socketPath?: string;
private server?: Server;
public constructor(serverFactory: HttpServerFactory, port: number) {
public constructor(serverFactory: HttpServerFactory, port?: number, socketPath?: string) {
super();
this.serverFactory = serverFactory;
this.port = port;
this.socketPath = socketPath;
if (!port && !socketPath) {
throw new Error('Either Port or Socket arguments must be set');
}
}
public async handle(): Promise<void> {
this.server = await this.serverFactory.createServer();
const url = new URL(`http${isHttpsServer(this.server) ? 's' : ''}://localhost:${this.port}/`).href;
this.logger.info(`Listening to server at ${url}`);
this.server.listen(this.port);
if (this.socketPath) {
this.logger.info(`Listening to server at ${this.server.address()}`);
this.server.listen(this.socketPath);
} else {
const url = new URL(`http${isHttpsServer(this.server) ? 's' : ''}://localhost:${this.port}/`).href;
this.logger.info(`Listening to server at ${url}`);
this.server.listen(this.port);
}
}
public async finalize(): Promise<void> {

View File

@ -18,6 +18,9 @@ export class BaseUrlExtractor extends ShorthandExtractor {
if (typeof args.baseUrl === 'string') {
return ensureTrailingSlash(args.baseUrl);
}
if (typeof args.socket === 'string') {
throw new Error('BaseUrl argument should be provided when using Unix Domain Sockets.');
}
const port = args.port ?? this.defaultPort;
return `http://localhost:${port}/`;
}

View File

@ -51,6 +51,7 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
return {
'urn:solid-server:default:variable:baseUrl': baseUrl ?? `http://localhost:${port}/`,
'urn:solid-server:default:variable:port': port,
'urn:solid-server:default:variable:socket': null,
'urn:solid-server:default:variable:loggingLevel': 'off',
'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:seededPodConfigJson': null,

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'));

View File

@ -20,6 +20,7 @@ describe('ServerInitializer', (): void => {
(getLoggerFor as jest.MockedFn<() => Logger>).mockReturnValue(logger);
server = {
address: jest.fn().mockResolvedValue('address'),
listen: jest.fn(),
close: jest.fn((fn: () => void): void => fn()),
} as any;
@ -49,6 +50,18 @@ describe('ServerInitializer', (): void => {
expect(logger.info).toHaveBeenLastCalledWith(`Listening to server at https://localhost:3000/`);
});
it('listens to the specified Unix Domain Socket.', async(): Promise<void> => {
initializer = new ServerInitializer(serverFactory, undefined, '/tmp/css.sock');
await initializer.handle();
expect(server.listen).toHaveBeenCalledWith('/tmp/css.sock');
});
it('throws when neither port or socket are set.', async(): Promise<void> => {
expect((): void => {
initializer = new ServerInitializer(serverFactory, undefined, undefined);
}).toThrow('Either Port or Socket arguments must be set');
});
it('can stop the server.', async(): Promise<void> => {
await initializer.handle();
await expect(initializer.finalize()).resolves.toBeUndefined();

View File

@ -16,6 +16,11 @@ describe('A BaseUrlExtractor', (): void => {
await expect(computer.handle({ port: 3333 })).resolves.toBe('http://localhost:3333/');
});
it('throws when a Unix Socket Path is provided without a baseUrl.', async(): Promise<void> => {
await expect(computer.handle({ socket: '/tmp/css.sock' })).rejects
.toThrow('BaseUrl argument should be provided when using Unix Domain Sockets.');
});
it('defaults to port 3000.', async(): Promise<void> => {
await expect(computer.handle({})).resolves.toBe('http://localhost:3000/');
});

View File

@ -37,6 +37,11 @@ const portNames = [
'BaseServerFactory',
] as const;
const socketNames = [
// Unit
'BaseHttpServerFactory',
];
export function getPort(name: typeof portNames[number]): number {
const idx = portNames.indexOf(name);
// Just in case something doesn't listen to the typings
@ -46,6 +51,15 @@ export function getPort(name: typeof portNames[number]): number {
return 6000 + idx;
}
export function getSocket(name: typeof socketNames[number]): string {
const idx = socketNames.indexOf(name);
// Just in case something doesn't listen to the typings
if (idx < 0) {
throw new Error(`Unknown socket name ${name}`);
}
return `css${idx}.sock`;
}
export function describeIf(envFlag: string): Describe {
const flag = `TEST_${envFlag.toUpperCase()}`;
const enabled = !/^(|0|false)$/iu.test(process.env[flag] ?? '');