deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View 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>

View 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"
}

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

View 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);
}
}

View 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);
}
}

View 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:/);
});
});

View 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;
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

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

View File

@@ -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);
}
}

View 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>;

View 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}`);
}
}
}

View File

@@ -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;
};

View 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>;

View File

@@ -0,0 +1,16 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../request"
}
]
}