deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
dev-packages/ovsx-client/.eslintrc.js
Normal file
10
dev-packages/ovsx-client/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
62
dev-packages/ovsx-client/README.md
Normal file
62
dev-packages/ovsx-client/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - OVSX CLIENT</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/ovsx-client` package is used to interact with `open-vsx` through its REST APIs.
|
||||
The package allows clients to fetch extensions and their metadata, search the registry, and
|
||||
includes the necessary logic to determine compatibility based on a provided supported API version.
|
||||
|
||||
Note that this client only supports a subset of the whole OpenVSX API, only what's relevant to
|
||||
clients like Theia applications.
|
||||
|
||||
### `OVSXRouterClient`
|
||||
|
||||
This class is an `OVSXClient` that can delegate requests to sub-clients based on some configuration (`OVSXRouterConfig`).
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"registries": {
|
||||
// `[Alias]: URL` pairs to avoid copy pasting URLs down the config
|
||||
},
|
||||
"use": [
|
||||
// List of aliases/URLs to use when no filtering was applied.
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ifRequestContains": "regex matched against various fields in requests",
|
||||
"ifExtensionIdMatches": "regex matched against the extension id (without version)",
|
||||
"use": [/*
|
||||
List of registries to forward the request to when all the
|
||||
conditions are matched.
|
||||
|
||||
`null` or `[]` means to not forward the request anywhere.
|
||||
*/]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
38
dev-packages/ovsx-client/package.json
Normal file
38
dev-packages/ovsx-client/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@theia/ovsx-client",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia Open-VSX Client",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@theia/request": "1.68.0",
|
||||
"limiter": "^2.1.0",
|
||||
"semver": "^7.5.4",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
22
dev-packages/ovsx-client/src/index.ts
Normal file
22
dev-packages/ovsx-client/src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export { OVSXApiFilter, OVSXApiFilterImpl, OVSXApiFilterProvider } from './ovsx-api-filter';
|
||||
export { OVSXHttpClient, OVSX_RATE_LIMIT } from './ovsx-http-client';
|
||||
export { OVSXMockClient } from './test/ovsx-mock-client';
|
||||
export { OVSXRouterClient, OVSXRouterConfig, OVSXRouterFilterFactory as FilterFactory } from './ovsx-router-client';
|
||||
export * from './ovsx-router-filters';
|
||||
export * from './ovsx-types';
|
||||
140
dev-packages/ovsx-client/src/ovsx-api-filter.ts
Normal file
140
dev-packages/ovsx-client/src/ovsx-api-filter.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as semver from 'semver';
|
||||
import { OVSXClient, VSXAllVersions, VSXBuiltinNamespaces, VSXExtensionRaw, VSXQueryOptions, VSXSearchEntry } from './ovsx-types';
|
||||
|
||||
export const OVSXApiFilterProvider = Symbol('OVSXApiFilterProvider');
|
||||
|
||||
export type OVSXApiFilterProvider = () => Promise<OVSXApiFilter>;
|
||||
|
||||
export const OVSXApiFilter = Symbol('OVSXApiFilter');
|
||||
/**
|
||||
* Filter various data types based on a pre-defined supported VS Code API version.
|
||||
*/
|
||||
export interface OVSXApiFilter {
|
||||
supportedApiVersion: string;
|
||||
findLatestCompatibleExtension(query: VSXQueryOptions): Promise<VSXExtensionRaw | undefined>;
|
||||
/**
|
||||
* Get the latest compatible extension version:
|
||||
* - A builtin extension is fetched based on the extension version which matches the API.
|
||||
* - An extension satisfies compatibility if its `engines.vscode` version is supported.
|
||||
*
|
||||
* @param extensionId the extension id.
|
||||
* @returns the data for the latest compatible extension version if available, else `undefined`.
|
||||
*/
|
||||
getLatestCompatibleExtension(extensions: VSXExtensionRaw[]): VSXExtensionRaw | undefined;
|
||||
getLatestCompatibleVersion(searchEntry: VSXSearchEntry): VSXAllVersions | undefined;
|
||||
}
|
||||
|
||||
export class OVSXApiFilterImpl implements OVSXApiFilter {
|
||||
|
||||
constructor(
|
||||
public client: OVSXClient,
|
||||
public supportedApiVersion: string
|
||||
) { }
|
||||
|
||||
async findLatestCompatibleExtension(query: VSXQueryOptions): Promise<VSXExtensionRaw | undefined> {
|
||||
const targetPlatform = query.targetPlatform;
|
||||
if (!targetPlatform) {
|
||||
return this.queryLatestCompatibleExtension(query);
|
||||
}
|
||||
const latestWithTargetPlatform = await this.queryLatestCompatibleExtension(query);
|
||||
let latestUniversal: VSXExtensionRaw | undefined;
|
||||
if (targetPlatform !== 'universal' && targetPlatform !== 'web') {
|
||||
// Additionally query the universal version, as there might be a newer one available
|
||||
latestUniversal = await this.queryLatestCompatibleExtension({ ...query, targetPlatform: 'universal' });
|
||||
}
|
||||
if (latestWithTargetPlatform && latestUniversal) {
|
||||
// Prefer the version with the target platform if it's greater or equal to the universal version
|
||||
return this.versionGreaterThanOrEqualTo(latestWithTargetPlatform.version, latestUniversal.version) ? latestWithTargetPlatform : latestUniversal;
|
||||
}
|
||||
return latestWithTargetPlatform ?? latestUniversal;
|
||||
}
|
||||
|
||||
protected async queryLatestCompatibleExtension(query: VSXQueryOptions): Promise<VSXExtensionRaw | undefined> {
|
||||
let offset = 0;
|
||||
let size = 5;
|
||||
let loop = true;
|
||||
while (loop) {
|
||||
const queryOptions: VSXQueryOptions = {
|
||||
...query,
|
||||
offset,
|
||||
size // there is a great chance that the newest version will work
|
||||
};
|
||||
const results = await this.client.query(queryOptions);
|
||||
const compatibleExtension = this.getLatestCompatibleExtension(results.extensions);
|
||||
if (compatibleExtension) {
|
||||
return compatibleExtension;
|
||||
}
|
||||
// Adjust offset by the amount of returned extensions
|
||||
offset += results.extensions.length;
|
||||
// Continue querying if there are more extensions available
|
||||
loop = results.totalSize > offset;
|
||||
// Adjust the size to fetch more extensions next time
|
||||
size = Math.min(size * 2, 100);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getLatestCompatibleExtension(extensions: VSXExtensionRaw[]): VSXExtensionRaw | undefined {
|
||||
if (extensions.length === 0) {
|
||||
return;
|
||||
} else if (this.isBuiltinNamespace(extensions[0].namespace.toLowerCase())) {
|
||||
return extensions.find(extension => this.versionGreaterThanOrEqualTo(this.supportedApiVersion, extension.version));
|
||||
} else {
|
||||
return extensions.find(extension => this.supportedVscodeApiSatisfies(extension.engines?.vscode ?? '*'));
|
||||
}
|
||||
}
|
||||
|
||||
getLatestCompatibleVersion(searchEntry: VSXSearchEntry): VSXAllVersions | undefined {
|
||||
function getLatestCompatibleVersion(predicate: (allVersions: VSXAllVersions) => boolean): VSXAllVersions | undefined {
|
||||
if (searchEntry.allVersions) {
|
||||
return searchEntry.allVersions.find(predicate);
|
||||
}
|
||||
// If the allVersions field is missing then try to use the
|
||||
// searchEntry as VSXAllVersions and check if it's compatible:
|
||||
if (predicate(searchEntry)) {
|
||||
return searchEntry;
|
||||
}
|
||||
}
|
||||
if (this.isBuiltinNamespace(searchEntry.namespace)) {
|
||||
return getLatestCompatibleVersion(allVersions => this.versionGreaterThanOrEqualTo(this.supportedApiVersion, allVersions.version));
|
||||
} else {
|
||||
return getLatestCompatibleVersion(allVersions => this.supportedVscodeApiSatisfies(allVersions.engines?.vscode ?? '*'));
|
||||
}
|
||||
}
|
||||
|
||||
protected isBuiltinNamespace(namespace: string): boolean {
|
||||
return VSXBuiltinNamespaces.is(namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `a >= b`
|
||||
*/
|
||||
protected versionGreaterThanOrEqualTo(a: string, b: string): boolean {
|
||||
const versionA = semver.clean(a);
|
||||
const versionB = semver.clean(b);
|
||||
if (!versionA || !versionB) {
|
||||
return false;
|
||||
}
|
||||
return semver.gte(versionA, versionB);
|
||||
}
|
||||
|
||||
protected supportedVscodeApiSatisfies(vscodeApiRange: string): boolean {
|
||||
return semver.satisfies(this.supportedApiVersion, vscodeApiRange);
|
||||
}
|
||||
}
|
||||
89
dev-packages/ovsx-client/src/ovsx-http-client.ts
Normal file
89
dev-packages/ovsx-client/src/ovsx-http-client.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { OVSXClient, VSXQueryOptions, VSXQueryResult, VSXSearchOptions, VSXSearchResult } from './ovsx-types';
|
||||
import { RequestContext, RequestService } from '@theia/request';
|
||||
import { RateLimiter } from 'limiter';
|
||||
|
||||
export const OVSX_RATE_LIMIT = 15;
|
||||
|
||||
export class OVSXHttpClient implements OVSXClient {
|
||||
|
||||
/**
|
||||
* @param requestService
|
||||
* @returns factory that will cache clients based on the requested input URL.
|
||||
*/
|
||||
static createClientFactory(requestService: RequestService, rateLimiter?: RateLimiter): (url: string) => OVSXClient {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const cachedClients: Record<string, OVSXClient> = Object.create(null);
|
||||
return url => cachedClients[url] ??= new this(url, requestService, rateLimiter);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected vsxRegistryUrl: string,
|
||||
protected requestService: RequestService,
|
||||
protected rateLimiter = new RateLimiter({ tokensPerInterval: OVSX_RATE_LIMIT, interval: 'second' })
|
||||
) { }
|
||||
|
||||
search(searchOptions?: VSXSearchOptions): Promise<VSXSearchResult> {
|
||||
return this.requestJson(this.buildUrl('api/-/search', searchOptions));
|
||||
}
|
||||
|
||||
query(queryOptions?: VSXQueryOptions): Promise<VSXQueryResult> {
|
||||
return this.requestJson(this.buildUrl('api/v2/-/query', queryOptions));
|
||||
}
|
||||
|
||||
protected async requestJson<R>(url: string): Promise<R> {
|
||||
const attempts = 5;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
// Use 1, 2, 4, 8, 16 tokens for each attempt
|
||||
const tokenCount = Math.pow(2, i);
|
||||
await this.rateLimiter.removeTokens(tokenCount);
|
||||
const context = await this.requestService.request({
|
||||
url,
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
if (context.res.statusCode === 429) {
|
||||
console.warn('OVSX rate limit exceeded. Consider reducing the rate limit.');
|
||||
// If there are still more attempts left, retry the request with a higher token count
|
||||
if (i < attempts - 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return RequestContext.asJson<R>(context);
|
||||
}
|
||||
throw new Error('Failed to fetch data from OVSX.');
|
||||
}
|
||||
|
||||
protected buildUrl(url: string, query?: object): string {
|
||||
return new URL(`${url}${this.buildQueryString(query)}`, this.vsxRegistryUrl).toString();
|
||||
}
|
||||
|
||||
protected buildQueryString(searchQuery?: object): string {
|
||||
if (!searchQuery) {
|
||||
return '';
|
||||
}
|
||||
let queryString = '';
|
||||
for (const [key, value] of Object.entries(searchQuery)) {
|
||||
if (typeof value === 'string') {
|
||||
queryString += `&${key}=${encodeURIComponent(value)}`;
|
||||
} else if (typeof value === 'boolean' || typeof value === 'number') {
|
||||
queryString += `&${key}=${value}`;
|
||||
}
|
||||
}
|
||||
return queryString && '?' + queryString.slice(1);
|
||||
}
|
||||
}
|
||||
126
dev-packages/ovsx-client/src/ovsx-router-client.spec.ts
Normal file
126
dev-packages/ovsx-client/src/ovsx-router-client.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable no-null/no-null */
|
||||
|
||||
import { OVSXRouterClient } from './ovsx-router-client';
|
||||
import { testClientProvider, registries, filterFactories } from './test/ovsx-router-client.spec-data';
|
||||
import { ExtensionLike } from './ovsx-types';
|
||||
import assert = require('assert');
|
||||
|
||||
describe('OVSXRouterClient', async () => {
|
||||
|
||||
const router = await OVSXRouterClient.FromConfig(
|
||||
{
|
||||
registries,
|
||||
use: ['internal', 'public', 'third'],
|
||||
rules: [{
|
||||
ifRequestContains: /\btestFullStop\b/.source,
|
||||
use: null,
|
||||
},
|
||||
{
|
||||
ifRequestContains: /\bsecret\b/.source,
|
||||
use: 'internal'
|
||||
},
|
||||
{
|
||||
ifExtensionIdMatches: /^some\./.source,
|
||||
use: 'internal'
|
||||
}]
|
||||
},
|
||||
testClientProvider,
|
||||
filterFactories,
|
||||
);
|
||||
|
||||
it('test query agglomeration', async () => {
|
||||
const result = await router.query({ namespaceName: 'other' });
|
||||
assert.deepStrictEqual(result.extensions.map(ExtensionLike.id), [
|
||||
// note the order: plugins from "internal" first then from "public"
|
||||
'other.d',
|
||||
'other.e'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test query request filtering', async () => {
|
||||
const result = await router.query({ namespaceName: 'secret' });
|
||||
assert.deepStrictEqual(result.extensions.map(ExtensionLike.id), [
|
||||
// 'secret.w' from 'public' shouldn't be returned
|
||||
'secret.x',
|
||||
'secret.y',
|
||||
'secret.z'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test query result filtering', async () => {
|
||||
const result = await router.query({ namespaceName: 'some' });
|
||||
assert.deepStrictEqual(result.extensions.map(ExtensionLike.idWithVersion), [
|
||||
// no entry for the `some` namespace should be returned from the `public` registry
|
||||
'some.a@1.0.0'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test query full stop', async () => {
|
||||
const result = await router.query({ extensionId: 'testFullStop.c' });
|
||||
assert.deepStrictEqual(result.extensions.length, 0);
|
||||
});
|
||||
|
||||
it('test search agglomeration', async () => {
|
||||
const result = await router.search({ query: 'other.' });
|
||||
assert.deepStrictEqual(result.extensions.map(ExtensionLike.id), [
|
||||
// note the order: plugins from "internal" first then from "public"
|
||||
'other.d',
|
||||
'other.e'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test search request filtering', async () => {
|
||||
const result = await router.search({ query: 'secret.' });
|
||||
assert.deepStrictEqual(result.extensions.map(ExtensionLike.id), [
|
||||
// 'secret.w' from 'public' shouldn't be returned
|
||||
'secret.x',
|
||||
'secret.y',
|
||||
'secret.z'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test search result filtering', async () => {
|
||||
const result = await router.search({ query: 'some.' });
|
||||
assert.deepStrictEqual(result.extensions.map(ExtensionLike.idWithVersion), [
|
||||
// no entry for the `some` namespace should be returned from the `public` registry
|
||||
'some.a@1.0.0'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test search full stop', async () => {
|
||||
const result = await router.search({ query: 'testFullStop.c' });
|
||||
assert.deepStrictEqual(result.extensions.length, 0);
|
||||
});
|
||||
|
||||
it('test config with unknown conditions', async () => {
|
||||
const clientPromise = OVSXRouterClient.FromConfig(
|
||||
{
|
||||
use: 'not relevant',
|
||||
rules: [{
|
||||
ifRequestContains: /.*/.source,
|
||||
unknownCondition: /should cause an error to be thrown/.source,
|
||||
use: ['internal', 'public']
|
||||
}]
|
||||
},
|
||||
testClientProvider,
|
||||
filterFactories
|
||||
);
|
||||
assert.rejects(clientPromise, /^Error: unknown conditions:/);
|
||||
});
|
||||
});
|
||||
253
dev-packages/ovsx-client/src/ovsx-router-client.ts
Normal file
253
dev-packages/ovsx-client/src/ovsx-router-client.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ExtensionLike, OVSXClient, OVSXClientProvider, VSXExtensionRaw, VSXQueryOptions, VSXQueryResult, VSXSearchEntry, VSXSearchOptions, VSXSearchResult } from './ovsx-types';
|
||||
import type { MaybePromise } from './types';
|
||||
|
||||
export interface OVSXRouterFilter {
|
||||
filterSearchOptions?(searchOptions?: VSXSearchOptions): MaybePromise<unknown>;
|
||||
filterQueryOptions?(queryOptions?: VSXQueryOptions): MaybePromise<unknown>;
|
||||
filterExtension?(extension: ExtensionLike): MaybePromise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param conditions key/value mapping of condition statements that rules may process
|
||||
* @param remainingKeys keys left to be processed, remove items from it when you handled them
|
||||
*/
|
||||
export type OVSXRouterFilterFactory = (conditions: Readonly<Record<string, unknown>>, remainingKeys: Set<string>) => MaybePromise<OVSXRouterFilter | undefined>;
|
||||
|
||||
/**
|
||||
* Helper function to create factories that handle a single condition key.
|
||||
*/
|
||||
export function createFilterFactory(conditionKey: string, factory: (conditionValue: unknown) => OVSXRouterFilter | undefined): OVSXRouterFilterFactory {
|
||||
return (conditions, remainingKeys) => {
|
||||
if (conditionKey in conditions) {
|
||||
const filter = factory(conditions[conditionKey]);
|
||||
if (filter) {
|
||||
remainingKeys.delete(conditionKey);
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface OVSXRouterConfig {
|
||||
/**
|
||||
* Registry aliases that will be used for routing.
|
||||
*/
|
||||
registries?: {
|
||||
[alias: string]: string
|
||||
}
|
||||
/**
|
||||
* The registry/ies to use by default.
|
||||
*/
|
||||
use: string | string[]
|
||||
/**
|
||||
* Filters for the different phases of interfacing with a registry.
|
||||
*/
|
||||
rules?: OVSXRouterRule[]
|
||||
}
|
||||
|
||||
export interface OVSXRouterRule {
|
||||
[condition: string]: unknown
|
||||
use?: string | string[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface OVSXRouterParsedRule {
|
||||
filters: OVSXRouterFilter[]
|
||||
use: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Route and agglomerate queries according to {@link routerConfig}.
|
||||
* {@link ruleFactories} is the actual logic used to evaluate the config.
|
||||
* Each rule implementation will be ran sequentially over each configured rule.
|
||||
*/
|
||||
export class OVSXRouterClient implements OVSXClient {
|
||||
|
||||
static async FromConfig(routerConfig: OVSXRouterConfig, clientProvider: OVSXClientProvider, filterFactories: OVSXRouterFilterFactory[]): Promise<OVSXRouterClient> {
|
||||
const rules = routerConfig.rules ? await this.ParseRules(routerConfig.rules, filterFactories, routerConfig.registries) : [];
|
||||
return new this(
|
||||
this.ParseUse(routerConfig.use, routerConfig.registries),
|
||||
clientProvider,
|
||||
rules
|
||||
);
|
||||
}
|
||||
|
||||
protected static async ParseRules(rules: OVSXRouterRule[], filterFactories: OVSXRouterFilterFactory[], aliases?: Record<string, string>): Promise<OVSXRouterParsedRule[]> {
|
||||
return Promise.all(rules.map(async ({ use, ...conditions }) => {
|
||||
const remainingKeys = new Set(Object.keys(conditions));
|
||||
const filters = removeNullValues(await Promise.all(filterFactories.map(filterFactory => filterFactory(conditions, remainingKeys))));
|
||||
if (remainingKeys.size > 0) {
|
||||
throw new Error(`unknown conditions: ${Array.from(remainingKeys).join(', ')}`);
|
||||
}
|
||||
return {
|
||||
filters,
|
||||
use: this.ParseUse(use, aliases)
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
protected static ParseUse(use: string | string[] | null | undefined, aliases?: Record<string, string>): string[] {
|
||||
if (typeof use === 'string') {
|
||||
return [alias(use)];
|
||||
} else if (Array.isArray(use)) {
|
||||
return use.map(alias);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
function alias(aliasOrUri: string): string {
|
||||
return aliases?.[aliasOrUri] ?? aliasOrUri;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected readonly useDefault: string[],
|
||||
protected readonly clientProvider: OVSXClientProvider,
|
||||
protected readonly rules: OVSXRouterParsedRule[],
|
||||
) { }
|
||||
|
||||
async search(searchOptions?: VSXSearchOptions): Promise<VSXSearchResult> {
|
||||
return this.runRules(
|
||||
filter => filter.filterSearchOptions?.(searchOptions),
|
||||
rule => rule.use.length > 0
|
||||
? this.mergedSearch(rule.use, searchOptions)
|
||||
: this.emptySearchResult(searchOptions),
|
||||
() => this.mergedSearch(this.useDefault, searchOptions)
|
||||
);
|
||||
}
|
||||
|
||||
async query(queryOptions: VSXQueryOptions = {}): Promise<VSXQueryResult> {
|
||||
return this.runRules(
|
||||
filter => filter.filterQueryOptions?.(queryOptions),
|
||||
rule => rule.use.length > 0
|
||||
? this.mergedQuery(rule.use, queryOptions)
|
||||
: this.emptyQueryResult(queryOptions),
|
||||
() => this.mergedQuery(this.useDefault, queryOptions)
|
||||
);
|
||||
}
|
||||
|
||||
protected emptySearchResult(searchOptions?: VSXSearchOptions): VSXSearchResult {
|
||||
return {
|
||||
extensions: [],
|
||||
offset: searchOptions?.offset ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
protected emptyQueryResult(queryOptions?: VSXQueryOptions): VSXQueryResult {
|
||||
return {
|
||||
offset: 0,
|
||||
totalSize: 0,
|
||||
extensions: []
|
||||
};
|
||||
}
|
||||
|
||||
protected async mergedQuery(registries: string[], queryOptions?: VSXQueryOptions): Promise<VSXQueryResult> {
|
||||
return this.mergeQueryResults(await createMapping(registries, async registry => (await this.clientProvider(registry)).query(queryOptions)));
|
||||
}
|
||||
|
||||
protected async mergedSearch(registries: string[], searchOptions?: VSXSearchOptions): Promise<VSXSearchResult> {
|
||||
return this.mergeSearchResults(await createMapping(registries, async registry => (await this.clientProvider(registry)).search(searchOptions)));
|
||||
}
|
||||
|
||||
protected async mergeSearchResults(results: Map<string, VSXSearchResult>): Promise<VSXSearchResult> {
|
||||
const filtering = [] as Promise<VSXSearchEntry[]>[];
|
||||
results.forEach((result, sourceUri) => {
|
||||
filtering.push(Promise
|
||||
.all(result.extensions.map(extension => this.filterExtension(sourceUri, extension)))
|
||||
.then(removeNullValues)
|
||||
);
|
||||
});
|
||||
return {
|
||||
extensions: interleave(await Promise.all(filtering)),
|
||||
offset: Math.min(...Array.from(results.values(), result => result.offset))
|
||||
};
|
||||
}
|
||||
|
||||
protected async mergeQueryResults(results: Map<string, VSXQueryResult>): Promise<VSXQueryResult> {
|
||||
const filtering = [] as Promise<VSXExtensionRaw | undefined>[];
|
||||
results.forEach((result, sourceUri) => {
|
||||
result.extensions.forEach(extension => filtering.push(this.filterExtension(sourceUri, extension)));
|
||||
});
|
||||
const extensions = removeNullValues(await Promise.all(filtering));
|
||||
return {
|
||||
offset: 0,
|
||||
totalSize: extensions.length,
|
||||
extensions
|
||||
};
|
||||
}
|
||||
|
||||
protected async filterExtension<T extends ExtensionLike>(sourceUri: string, extension: T): Promise<T | undefined> {
|
||||
return this.runRules(
|
||||
filter => filter.filterExtension?.(extension),
|
||||
rule => rule.use.includes(sourceUri) ? extension : undefined,
|
||||
() => extension
|
||||
);
|
||||
}
|
||||
|
||||
protected runRules<T>(runFilter: (filter: OVSXRouterFilter) => unknown, onRuleMatched: (rule: OVSXRouterParsedRule) => T): Promise<T | undefined>;
|
||||
protected runRules<T, U>(runFilter: (filter: OVSXRouterFilter) => unknown, onRuleMatched: (rule: OVSXRouterParsedRule) => T, onNoRuleMatched: () => U): Promise<T | U>;
|
||||
protected async runRules<T, U>(
|
||||
runFilter: (filter: OVSXRouterFilter) => unknown,
|
||||
onRuleMatched: (rule: OVSXRouterParsedRule) => T,
|
||||
onNoRuleMatched?: () => U
|
||||
): Promise<T | U | undefined> {
|
||||
for (const rule of this.rules) {
|
||||
const results = removeNullValues(await Promise.all(rule.filters.map(filter => runFilter(filter))));
|
||||
if (results.length > 0 && results.every(value => value)) {
|
||||
return onRuleMatched(rule);
|
||||
}
|
||||
}
|
||||
return onNoRuleMatched?.();
|
||||
}
|
||||
}
|
||||
|
||||
function nonNullable<T>(value: T | null | undefined): value is T {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return typeof value !== 'undefined' && value !== null;
|
||||
}
|
||||
|
||||
function removeNullValues<T>(values: (T | null | undefined)[]): T[] {
|
||||
return values.filter(nonNullable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a map where the keys are each element from {@link values} and the
|
||||
* values are the result of a mapping function applied on the key.
|
||||
*/
|
||||
async function createMapping<T, U>(values: T[], map: (value: T, index: number) => MaybePromise<U>, thisArg?: unknown): Promise<Map<T, U>> {
|
||||
return new Map(await Promise.all(values.map(async (value, index) => [value, await map.call(thisArg, value, index)] as [T, U])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* interleave([[1, 2, 3], [4, 5], [6, 7, 8]]) === [1, 4, 6, 2, 5, 7, 3, 8]
|
||||
*/
|
||||
function interleave<T>(arrays: T[][]): T[] {
|
||||
const interleaved: T[] = [];
|
||||
const length = Math.max(...arrays.map(array => array.length));
|
||||
for (let i = 0; i < length; i++) {
|
||||
for (const array of arrays) {
|
||||
if (i < array.length) {
|
||||
interleaved.push(array[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return interleaved;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export abstract class AbstractRegExpFilter {
|
||||
|
||||
constructor(
|
||||
protected regExp: RegExp
|
||||
) { }
|
||||
|
||||
protected test(value: unknown): boolean {
|
||||
return typeof value === 'string' && this.regExp.test(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { createFilterFactory, OVSXRouterFilter } from '../ovsx-router-client';
|
||||
import { ExtensionLike } from '../ovsx-types';
|
||||
import { AbstractRegExpFilter } from './abstract-reg-exp-filter';
|
||||
|
||||
export const ExtensionIdMatchesFilterFactory = createFilterFactory('ifExtensionIdMatches', ifExtensionIdMatches => {
|
||||
if (typeof ifExtensionIdMatches !== 'string') {
|
||||
throw new TypeError(`expected a string, got: ${typeof ifExtensionIdMatches}`);
|
||||
}
|
||||
return new ExtensionIdMatchesFilter(new RegExp(ifExtensionIdMatches, 'i'));
|
||||
});
|
||||
|
||||
export class ExtensionIdMatchesFilter extends AbstractRegExpFilter implements OVSXRouterFilter {
|
||||
filterExtension(extension: ExtensionLike): boolean {
|
||||
return this.test(ExtensionLike.id(extension));
|
||||
}
|
||||
}
|
||||
18
dev-packages/ovsx-client/src/ovsx-router-filters/index.ts
Normal file
18
dev-packages/ovsx-client/src/ovsx-router-filters/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export { ExtensionIdMatchesFilterFactory } from './extension-id-matches-filter';
|
||||
export { RequestContainsFilterFactory } from './request-contains-filter';
|
||||
@@ -0,0 +1,35 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { createFilterFactory, OVSXRouterFilter } from '../ovsx-router-client';
|
||||
import { VSXQueryOptions, VSXSearchOptions } from '../ovsx-types';
|
||||
import { AbstractRegExpFilter } from './abstract-reg-exp-filter';
|
||||
|
||||
export const RequestContainsFilterFactory = createFilterFactory('ifRequestContains', ifRequestContains => {
|
||||
if (typeof ifRequestContains !== 'string') {
|
||||
throw new TypeError(`expected a string, got: ${typeof ifRequestContains}`);
|
||||
}
|
||||
return new RequestContainsFilter(new RegExp(ifRequestContains, 'i'));
|
||||
});
|
||||
|
||||
export class RequestContainsFilter extends AbstractRegExpFilter implements OVSXRouterFilter {
|
||||
filterSearchOptions(searchOptions?: VSXSearchOptions): boolean {
|
||||
return !searchOptions || this.test(searchOptions.query) || this.test(searchOptions.category);
|
||||
}
|
||||
filterQueryOptions(queryOptions?: VSXQueryOptions): boolean {
|
||||
return !queryOptions || Object.values(queryOptions).some(this.test, this);
|
||||
}
|
||||
}
|
||||
309
dev-packages/ovsx-client/src/ovsx-types.ts
Normal file
309
dev-packages/ovsx-client/src/ovsx-types.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { MaybePromise } from './types';
|
||||
|
||||
export interface ExtensionLike {
|
||||
name: string;
|
||||
namespace: string;
|
||||
version?: string;
|
||||
}
|
||||
export namespace ExtensionLike {
|
||||
export function id<T extends ExtensionLike>(extension: T): `${string}.${string}` {
|
||||
return `${extension.namespace}.${extension.name}`;
|
||||
}
|
||||
export function idWithVersion<T extends ExtensionLike>(extension: T): `${string}.${string}@${string}` {
|
||||
if (!extension.version) {
|
||||
throw new Error(`no valid "version" value provided for "${id(extension)}"`);
|
||||
}
|
||||
return `${id(extension)}@${extension.version}`;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export function fromId(id: string): ExtensionLike {
|
||||
const [left, version] = id.split('@', 2);
|
||||
const [namespace, name] = left.split('.', 2);
|
||||
return {
|
||||
name,
|
||||
namespace,
|
||||
version
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface OVSXClient {
|
||||
/**
|
||||
* GET https://openvsx.org/api/-/search
|
||||
*/
|
||||
search(searchOptions?: VSXSearchOptions): Promise<VSXSearchResult>;
|
||||
/**
|
||||
* GET https://openvsx.org/api/v2/-/query
|
||||
*
|
||||
* Fetch one or all versions of an extension.
|
||||
*/
|
||||
query(queryOptions?: VSXQueryOptions): Promise<VSXQueryResult>;
|
||||
}
|
||||
|
||||
/** @deprecated since 1.31.0 use {@link VSXSearchOptions} instead */
|
||||
export type VSXSearchParam = VSXSearchOptions;
|
||||
/**
|
||||
* The possible options when performing a search.
|
||||
*
|
||||
* For available options, and default values consult the `swagger`: https://open-vsx.org/swagger-ui/index.html.
|
||||
*
|
||||
* Should be aligned with https://github.com/eclipse/openvsx/blob/b5694a712e07d266801394916bac30609e16d77b/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java#L246-L266
|
||||
*/
|
||||
export interface VSXSearchOptions {
|
||||
/**
|
||||
* The query text for searching.
|
||||
*/
|
||||
query?: string;
|
||||
/**
|
||||
* The extension category.
|
||||
*/
|
||||
category?: string;
|
||||
/**
|
||||
* The maximum number of entries to return.
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* The number of entries to skip (usually a multiple of the page size).
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* The sort order.
|
||||
*/
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
/**
|
||||
* The sort key.
|
||||
*/
|
||||
sortBy?: 'averageRating' | 'downloadCount' | 'relevance' | 'timestamp';
|
||||
/**
|
||||
* By default an OpenVSX registry will return the last known version of
|
||||
* extensions. Setting this field to `true` will have the registry specify
|
||||
* the {@link VSXExtensionRaw.allVersions} field which references all known
|
||||
* versions for each returned extension.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
includeAllVersions?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be aligned with https://github.com/eclipse/openvsx/blob/e8f64fe145fc05d2de1469735d50a7a90e400bc4/server/src/main/java/org/eclipse/openvsx/json/SearchResultJson.java
|
||||
*/
|
||||
export interface VSXSearchResult {
|
||||
offset: number;
|
||||
extensions: VSXSearchEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible options when performing a search.
|
||||
*
|
||||
* For available options, and default values consult the `swagger`: https://open-vsx.org/swagger-ui/index.html.
|
||||
*
|
||||
* Should be aligned with https://github.com/eclipse/openvsx/blob/b5694a712e07d266801394916bac30609e16d77b/server/src/main/java/org/eclipse/openvsx/json/QueryParamJson.java#L18-L46
|
||||
*/
|
||||
export interface VSXQueryOptions {
|
||||
namespaceName?: string;
|
||||
extensionName?: string;
|
||||
extensionVersion?: string;
|
||||
extensionId?: string;
|
||||
extensionUuid?: string;
|
||||
namespaceUuid?: string;
|
||||
includeAllVersions?: boolean | 'links';
|
||||
targetPlatform?: VSXTargetPlatform;
|
||||
size?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export type VSXTargetPlatform =
|
||||
'universal' | 'web' |
|
||||
'win32-x64' | 'win32-ia32' | 'win32-arm64' |
|
||||
'darwin-x64' | 'darwin-arm64' |
|
||||
'linux-x64' | 'linux-arm64' | 'linux-armhf' |
|
||||
'alpine-x64' | 'alpine-arm64' | (string & {});
|
||||
|
||||
export interface VSXQueryResult {
|
||||
success?: string;
|
||||
warning?: string;
|
||||
error?: string;
|
||||
offset: number;
|
||||
totalSize: number;
|
||||
extensions: VSXExtensionRaw[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This type describes the data as found in {@link VSXSearchEntry.allVersions}.
|
||||
*
|
||||
* Note that this type only represents one version of a given plugin, despite the name.
|
||||
*/
|
||||
export interface VSXAllVersions {
|
||||
url: string;
|
||||
version: string;
|
||||
engines?: {
|
||||
[version: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be aligned with https://github.com/eclipse/openvsx/blob/master/server/src/main/java/org/eclipse/openvsx/json/SearchEntryJson.java
|
||||
*/
|
||||
export interface VSXSearchEntry {
|
||||
url: string;
|
||||
files: {
|
||||
download: string;
|
||||
manifest?: string;
|
||||
readme?: string;
|
||||
license?: string;
|
||||
icon?: string;
|
||||
};
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
timestamp: string;
|
||||
averageRating?: number;
|
||||
downloadCount: number;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
/**
|
||||
* May be undefined when {@link VSXSearchOptions.includeAllVersions} is
|
||||
* `false` or `undefined`.
|
||||
*/
|
||||
allVersions?: VSXAllVersions[];
|
||||
}
|
||||
|
||||
export type VSXExtensionNamespaceAccess = 'public' | 'restricted';
|
||||
|
||||
/**
|
||||
* Should be aligned with https://github.com/eclipse/openvsx/blob/master/server/src/main/java/org/eclipse/openvsx/json/UserJson.java
|
||||
*/
|
||||
export interface VSXUser {
|
||||
loginName: string;
|
||||
homepage?: string;
|
||||
}
|
||||
|
||||
export interface VSXExtensionRawFiles {
|
||||
download: string;
|
||||
readme?: string;
|
||||
license?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be aligned with https://github.com/eclipse/openvsx/blob/master/server/src/main/java/org/eclipse/openvsx/json/ExtensionJson.java
|
||||
*/
|
||||
export interface VSXExtensionRaw {
|
||||
error?: string;
|
||||
namespaceUrl: string;
|
||||
reviewsUrl: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
targetPlatform?: VSXTargetPlatform;
|
||||
publishedBy: VSXUser;
|
||||
preRelease: boolean;
|
||||
namespaceAccess: VSXExtensionNamespaceAccess;
|
||||
files: VSXExtensionRawFiles;
|
||||
allVersions: {
|
||||
[version: string]: string;
|
||||
};
|
||||
allVersionsUrl?: string;
|
||||
averageRating?: number;
|
||||
downloadCount: number;
|
||||
reviewCount: number;
|
||||
version: string;
|
||||
timestamp: string;
|
||||
preview?: boolean;
|
||||
verified?: boolean;
|
||||
displayName?: string;
|
||||
namespaceDisplayName: string;
|
||||
description?: string;
|
||||
categories?: string[];
|
||||
extensionKind?: string[];
|
||||
tags?: string[];
|
||||
license?: string;
|
||||
homepage?: string;
|
||||
repository?: string;
|
||||
sponsorLink?: string;
|
||||
bugs?: string;
|
||||
markdown?: string;
|
||||
galleryColor?: string;
|
||||
galleryTheme?: string;
|
||||
localizedLanguages?: string[];
|
||||
qna?: string;
|
||||
badges?: VSXBadge[];
|
||||
dependencies?: VSXExtensionReference[];
|
||||
bundledExtensions?: VSXExtensionReference[];
|
||||
allTargetPlatformVersions?: VSXTargetPlatforms[];
|
||||
url?: string;
|
||||
engines?: {
|
||||
[engine: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VSXBadge {
|
||||
url?: string;
|
||||
href?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface VSXExtensionReference {
|
||||
url: string;
|
||||
namespace: string;
|
||||
extension: string;
|
||||
}
|
||||
|
||||
export interface VSXTargetPlatforms {
|
||||
version: string;
|
||||
targetPlatforms: VSXTargetPlatform[];
|
||||
}
|
||||
|
||||
export interface VSXResponseError extends Error {
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export namespace VSXResponseError {
|
||||
export function is(error: unknown): error is VSXResponseError {
|
||||
return !!error && typeof error === 'object' && typeof (error as VSXResponseError).statusCode === 'number';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builtin namespaces maintained by the framework.
|
||||
*/
|
||||
export namespace VSXBuiltinNamespaces {
|
||||
|
||||
/**
|
||||
* Namespace for individual vscode builtin extensions.
|
||||
*/
|
||||
export const VSCODE = 'vscode';
|
||||
|
||||
/**
|
||||
* Namespace for vscode builtin extension packs.
|
||||
* - corresponds to: https://github.com/eclipse-theia/vscode-builtin-extensions/blob/af9cfeb2ea23e1668a8340c1c2fb5afd56be07d7/src/create-extension-pack.js#L45
|
||||
*/
|
||||
export const THEIA = 'eclipse-theia';
|
||||
|
||||
/**
|
||||
* Determines if the extension namespace is a builtin maintained by the framework.
|
||||
* @param namespace the extension namespace to verify.
|
||||
*/
|
||||
export function is(namespace: string): boolean {
|
||||
return namespace === VSCODE
|
||||
|| namespace === THEIA;
|
||||
}
|
||||
}
|
||||
|
||||
export type OVSXClientProvider = (uri: string) => MaybePromise<OVSXClient>;
|
||||
187
dev-packages/ovsx-client/src/test/ovsx-mock-client.ts
Normal file
187
dev-packages/ovsx-client/src/test/ovsx-mock-client.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ExtensionLike, OVSXClient, VSXExtensionRaw, VSXQueryOptions, VSXQueryResult, VSXSearchOptions, VSXSearchResult } from '../ovsx-types';
|
||||
|
||||
/**
|
||||
* Querying will only find exact matches.
|
||||
* Searching will try to find the query string in various fields.
|
||||
*/
|
||||
export class OVSXMockClient implements OVSXClient {
|
||||
|
||||
constructor(
|
||||
public extensions: VSXExtensionRaw[] = []
|
||||
) { }
|
||||
|
||||
setExtensions(extensions: VSXExtensionRaw[]): this {
|
||||
this.extensions = extensions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param baseUrl required to construct the URLs required by {@link VSXExtensionRaw}.
|
||||
* @param ids list of ids to generate {@link VSXExtensionRaw} from.
|
||||
*/
|
||||
setExtensionsFromIds(baseUrl: string, ids: string[]): this {
|
||||
const now = Date.now();
|
||||
const url = new OVSXMockClient.UrlBuilder(baseUrl);
|
||||
this.extensions = ids.map((extension, i) => {
|
||||
const [id, version = '0.0.1'] = extension.split('@', 2);
|
||||
const [namespace, name] = id.split('.', 2);
|
||||
return {
|
||||
allVersions: {
|
||||
[version]: url.extensionUrl(namespace, name, `/${version}`)
|
||||
},
|
||||
displayName: name,
|
||||
downloadCount: 0,
|
||||
files: {
|
||||
download: url.extensionFileUrl(namespace, name, version, `/${id}-${version}.vsix`)
|
||||
},
|
||||
name,
|
||||
namespace,
|
||||
namespaceAccess: 'public',
|
||||
namespaceUrl: url.namespaceUrl(namespace),
|
||||
publishedBy: {
|
||||
loginName: 'mock'
|
||||
},
|
||||
reviewCount: 0,
|
||||
reviewsUrl: url.extensionReviewsUrl(namespace, name),
|
||||
timestamp: new Date(now - ids.length + i + 1).toISOString(),
|
||||
version,
|
||||
description: `Mock VS Code Extension for ${id}`,
|
||||
namespaceDisplayName: name,
|
||||
preRelease: false
|
||||
};
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
async query(queryOptions?: VSXQueryOptions): Promise<VSXQueryResult> {
|
||||
const extensions = this.extensions
|
||||
.filter(extension => typeof queryOptions === 'object' && (
|
||||
this.compare(queryOptions.extensionId, this.id(extension)) &&
|
||||
this.compare(queryOptions.extensionName, extension.name) &&
|
||||
this.compare(queryOptions.extensionVersion, extension.version) &&
|
||||
this.compare(queryOptions.namespaceName, extension.namespace)
|
||||
));
|
||||
return {
|
||||
offset: 0,
|
||||
totalSize: extensions.length,
|
||||
extensions
|
||||
};
|
||||
}
|
||||
|
||||
async search(searchOptions?: VSXSearchOptions): Promise<VSXSearchResult> {
|
||||
const query = searchOptions?.query;
|
||||
const offset = searchOptions?.offset ?? 0;
|
||||
const size = searchOptions?.size ?? 18;
|
||||
const end = offset + size;
|
||||
return {
|
||||
offset,
|
||||
extensions: this.extensions
|
||||
.filter(extension => typeof query !== 'string' || (
|
||||
this.includes(query, this.id(extension)) ||
|
||||
this.includes(query, extension.description) ||
|
||||
this.includes(query, extension.displayName)
|
||||
))
|
||||
.sort((a, b) => this.sort(a, b, searchOptions))
|
||||
.filter((extension, i) => i >= offset && i < end)
|
||||
.map(extension => ({
|
||||
downloadCount: extension.downloadCount,
|
||||
files: extension.files,
|
||||
name: extension.name,
|
||||
namespace: extension.namespace,
|
||||
timestamp: extension.timestamp,
|
||||
url: `${extension.namespaceUrl}/${extension.name}`,
|
||||
version: extension.version,
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
protected id(extension: ExtensionLike): string {
|
||||
return `${extension.namespace}.${extension.name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Case sensitive.
|
||||
*/
|
||||
protected compare(expected?: string, value?: string): boolean {
|
||||
return expected === undefined || value === undefined || expected === value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Case insensitive.
|
||||
*/
|
||||
protected includes(needle: string, value?: string): boolean {
|
||||
return value === undefined || value.toLowerCase().includes(needle.toLowerCase());
|
||||
}
|
||||
|
||||
protected sort(a: VSXExtensionRaw, b: VSXExtensionRaw, searchOptions?: VSXSearchOptions): number {
|
||||
let order: number = 0;
|
||||
const sortBy = searchOptions?.sortBy ?? 'relevance';
|
||||
const sortOrder = searchOptions?.sortOrder ?? 'desc';
|
||||
if (sortBy === 'averageRating') {
|
||||
order = (a.averageRating ?? -1) - (b.averageRating ?? -1);
|
||||
} else if (sortBy === 'downloadCount') {
|
||||
order = a.downloadCount - b.downloadCount;
|
||||
} else if (sortBy === 'relevance') {
|
||||
order = 0;
|
||||
} else if (sortBy === 'timestamp') {
|
||||
order = new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
|
||||
}
|
||||
if (sortOrder === 'asc') {
|
||||
order *= -1;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
}
|
||||
export namespace OVSXMockClient {
|
||||
|
||||
/**
|
||||
* URLs should respect the official OpenVSX API:
|
||||
* https://open-vsx.org/swagger-ui/index.html
|
||||
*/
|
||||
export class UrlBuilder {
|
||||
|
||||
constructor(
|
||||
protected baseUrl: string
|
||||
) { }
|
||||
|
||||
url(path: string): string {
|
||||
return this.baseUrl + path;
|
||||
}
|
||||
|
||||
apiUrl(path: string): string {
|
||||
return this.url(`/api${path}`);
|
||||
}
|
||||
|
||||
namespaceUrl(namespace: string, path = ''): string {
|
||||
return this.apiUrl(`/${namespace}${path}`);
|
||||
}
|
||||
|
||||
extensionUrl(namespace: string, name: string, path = ''): string {
|
||||
return this.apiUrl(`/${namespace}/${name}${path}`);
|
||||
}
|
||||
|
||||
extensionReviewsUrl(namespace: string, name: string): string {
|
||||
return this.apiUrl(`/${namespace}/${name}/reviews`);
|
||||
}
|
||||
|
||||
extensionFileUrl(namespace: string, name: string, version: string, path = ''): string {
|
||||
return this.apiUrl(`/${namespace}/${name}/${version}/file${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable no-null/no-null */
|
||||
|
||||
import { OVSXMockClient } from './ovsx-mock-client';
|
||||
import { ExtensionIdMatchesFilterFactory, RequestContainsFilterFactory } from '../ovsx-router-filters';
|
||||
import { OVSXClient } from '../ovsx-types';
|
||||
|
||||
export const registries = {
|
||||
internal: 'https://internal.testdomain/',
|
||||
public: 'https://public.testdomain/',
|
||||
third: 'https://third.testdomain/'
|
||||
};
|
||||
|
||||
export const clients: Record<string, OVSXMockClient> = {
|
||||
[registries.internal]: new OVSXMockClient().setExtensionsFromIds(registries.internal, [
|
||||
'some.a@1.0.0',
|
||||
'other.d',
|
||||
'secret.x',
|
||||
'secret.y',
|
||||
'secret.z',
|
||||
...Array(50)
|
||||
.fill(undefined)
|
||||
.map((element, i) => `internal.autogen${i}`)
|
||||
]),
|
||||
[registries.public]: new OVSXMockClient().setExtensionsFromIds(registries.public, [
|
||||
'some.a@2.0.0',
|
||||
'some.b',
|
||||
'other.e',
|
||||
'testFullStop.c',
|
||||
'secret.w',
|
||||
...Array(50)
|
||||
.fill(undefined)
|
||||
.map((element, i) => `public.autogen${i}`)
|
||||
]),
|
||||
[registries.third]: new OVSXMockClient().setExtensionsFromIds(registries.third, [
|
||||
...Array(200)
|
||||
.fill(undefined)
|
||||
.map((element, i) => `third.autogen${i}`)
|
||||
])
|
||||
};
|
||||
|
||||
export const filterFactories = [
|
||||
RequestContainsFilterFactory,
|
||||
ExtensionIdMatchesFilterFactory
|
||||
];
|
||||
|
||||
export function testClientProvider(uri: string): OVSXClient {
|
||||
const client = clients[uri];
|
||||
if (!client) {
|
||||
throw new Error(`unknown client for URI=${uri}`);
|
||||
}
|
||||
return client;
|
||||
};
|
||||
17
dev-packages/ovsx-client/src/types.ts
Normal file
17
dev-packages/ovsx-client/src/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export type MaybePromise<T> = T | PromiseLike<T>;
|
||||
16
dev-packages/ovsx-client/tsconfig.json
Normal file
16
dev-packages/ovsx-client/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../request"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user