diff --git a/.gitignore b/.gitignore index 2d7258711e8ba..5bdb44c92a06d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ dependency-check-summary.txt* *-trace.json .tours /performance-result.json +*.vsix diff --git a/.vscode/launch.json b/.vscode/launch.json index 312aa0da0243a..c171fa1f15233 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,7 +34,8 @@ "--app-project-path=${workspaceFolder}/examples/electron", "--remote-debugging-port=9222", "--no-app-auto-install", - "--plugins=local-dir:../../plugins" + "--plugins=local-dir:../../plugins", + "--ovsx-router-config=examples/ovsx-router-config.json" ], "env": { "NODE_ENV": "development" @@ -44,6 +45,7 @@ "${workspaceFolder}/examples/electron/lib/backend/electron-main.js", "${workspaceFolder}/examples/electron/lib/backend/main.js", "${workspaceFolder}/examples/electron/lib/**/*.js", + "${workspaceFolder}/examples/api-samples/lib/**/*.js", "${workspaceFolder}/packages/*/lib/**/*.js", "${workspaceFolder}/dev-packages/*/lib/**/*.js" ], @@ -62,7 +64,8 @@ "--no-cluster", "--app-project-path=${workspaceFolder}/examples/browser", "--plugins=local-dir:plugins", - "--hosted-plugin-inspect=9339" + "--hosted-plugin-inspect=9339", + "--ovsx-router-config=examples/ovsx-router-config.json" ], "env": { "NODE_ENV": "development" @@ -71,6 +74,7 @@ "outFiles": [ "${workspaceFolder}/examples/browser/src-gen/backend/*.js", "${workspaceFolder}/examples/browser/lib/**/*.js", + "${workspaceFolder}/examples/api-samples/lib/**/*.js", "${workspaceFolder}/packages/*/lib/**/*.js", "${workspaceFolder}/dev-packages/*/lib/**/*.js" ], diff --git a/configs/base.tsconfig.json b/configs/base.tsconfig.json index 08a3f4a6010e1..d6b6e42db7c55 100644 --- a/configs/base.tsconfig.json +++ b/configs/base.tsconfig.json @@ -14,14 +14,14 @@ "emitDecoratorMetadata": true, "downlevelIteration": true, "resolveJsonModule": true, - "module": "commonjs", - "moduleResolution": "node", - "target": "ES2017", + "module": "CommonJS", + "moduleResolution": "Node", + "target": "ES2019", "jsx": "react", "lib": [ - "ES2017", + "ES2019", "ES2020.Promise", - "dom" + "DOM" ], "sourceMap": true } diff --git a/dev-packages/cli/src/download-plugins.ts b/dev-packages/cli/src/download-plugins.ts index e68c00f72d68e..a3a39ef63f137 100644 --- a/dev-packages/cli/src/download-plugins.ts +++ b/dev-packages/cli/src/download-plugins.ts @@ -16,22 +16,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -declare global { - interface Array { - // Supported since Node >=11.0 - flat(depth?: number): any - } -} - -import { OVSXClient } from '@theia/ovsx-client/lib/ovsx-client'; +import { OVSXApiFilterImpl, OVSXClient } from '@theia/ovsx-client'; import * as chalk from 'chalk'; import * as decompress from 'decompress'; import { promises as fs } from 'fs'; import * as path from 'path'; import * as temp from 'temp'; -import { NodeRequestService } from '@theia/request/lib/node-request-service'; import { DEFAULT_SUPPORTED_API_VERSION } from '@theia/application-package/lib/api'; -import { RequestContext } from '@theia/request'; +import { RequestContext, RequestService } from '@theia/request'; import { RateLimiter } from 'limiter'; import escapeStringRegexp = require('escape-string-regexp'); @@ -59,21 +51,12 @@ export interface DownloadPluginsOptions { */ apiVersion?: string; - /** - * The open-vsx registry API url. - */ - apiUrl?: string; - /** * Fetch plugins in parallel */ parallel?: boolean; rateLimit?: number; - - proxyUrl?: string; - proxyAuthorization?: string; - strictSsl?: boolean; } interface PluginDownload { @@ -82,26 +65,17 @@ interface PluginDownload { version?: string | undefined } -const requestService = new NodeRequestService(); - -export default async function downloadPlugins(options: DownloadPluginsOptions = {}): Promise { +export default async function downloadPlugins(ovsxClient: OVSXClient, requestService: RequestService, options: DownloadPluginsOptions = {}): Promise { const { packed = false, ignoreErrors = false, apiVersion = DEFAULT_SUPPORTED_API_VERSION, - apiUrl = 'https://open-vsx.org/api', - parallel = true, rateLimit = 15, - proxyUrl, - proxyAuthorization, - strictSsl + parallel = true } = options; - requestService.configure({ - proxyUrl, - proxyAuthorization, - strictSSL: strictSsl - }); + const rateLimiter = new RateLimiter({ tokensPerInterval: rateLimit, interval: 'second' }); + const apiFilter = new OVSXApiFilterImpl(apiVersion); // Collect the list of failures to be appended at the end of the script. const failures: string[] = []; @@ -115,7 +89,7 @@ export default async function downloadPlugins(options: DownloadPluginsOptions = // Excluded extension ids. const excludedIds = new Set(pck.theiaPluginsExcludeIds || []); - const parallelOrSequence = async (...tasks: Array<() => unknown>) => { + const parallelOrSequence = async (tasks: (() => unknown)[]) => { if (parallel) { await Promise.all(tasks.map(task => task())); } else { @@ -125,13 +99,13 @@ export default async function downloadPlugins(options: DownloadPluginsOptions = } }; - const rateLimiter = new RateLimiter({ tokensPerInterval: rateLimit, interval: 'second' }); - // Downloader wrapper - const downloadPlugin = (plugin: PluginDownload): Promise => downloadPluginAsync(rateLimiter, failures, plugin.id, plugin.downloadUrl, pluginsDir, packed, plugin.version); + const downloadPlugin = async (plugin: PluginDownload): Promise => { + await downloadPluginAsync(requestService, rateLimiter, failures, plugin.id, plugin.downloadUrl, pluginsDir, packed, plugin.version); + }; const downloader = async (plugins: PluginDownload[]) => { - await parallelOrSequence(...plugins.map(plugin => () => downloadPlugin(plugin))); + await parallelOrSequence(plugins.map(plugin => () => downloadPlugin(plugin))); }; await fs.mkdir(pluginsDir, { recursive: true }); @@ -146,17 +120,17 @@ export default async function downloadPlugins(options: DownloadPluginsOptions = // This will include both "normal" plugins as well as "extension packs". const pluginsToDownload = Object.entries(pck.theiaPlugins) .filter((entry: [string, unknown]): entry is [string, string] => typeof entry[1] === 'string') - .map(([pluginId, url]) => ({ id: pluginId, downloadUrl: resolveDownloadUrlPlaceholders(url) })); + .map(([id, url]) => ({ id, downloadUrl: resolveDownloadUrlPlaceholders(url) })); await downloader(pluginsToDownload); - const handleDependencyList = async (dependencies: Array) => { - const client = new OVSXClient({ apiVersion, apiUrl }, requestService); + const handleDependencyList = async (dependencies: (string | string[])[]) => { // De-duplicate extension ids to only download each once: const ids = new Set(dependencies.flat()); - await parallelOrSequence(...Array.from(ids, id => async () => { + await parallelOrSequence(Array.from(ids, id => async () => { try { await rateLimiter.removeTokens(1); - const extension = await client.getLatestCompatibleExtensionVersion(id); + const { extensions } = await ovsxClient.query({ extensionId: id }); + const extension = apiFilter.getLatestCompatibleExtension(extensions); const version = extension?.version; const downloadUrl = extension?.files.download; if (downloadUrl) { @@ -208,14 +182,15 @@ function resolveDownloadUrlPlaceholders(url: string): string { /** * Downloads a plugin, will make multiple attempts before actually failing. + * @param requestService * @param failures reference to an array storing all failures. * @param plugin plugin short name. * @param pluginUrl url to download the plugin at. * @param target where to download the plugin in. * @param packed whether to decompress or not. - * @param cachedExtensionPacks the list of cached extension packs already downloaded. */ async function downloadPluginAsync( + requestService: RequestService, rateLimiter: RateLimiter, failures: string[], plugin: string, diff --git a/dev-packages/cli/src/theia.ts b/dev-packages/cli/src/theia.ts index dea322c4e3a23..416707f073d54 100644 --- a/dev-packages/cli/src/theia.ts +++ b/dev-packages/cli/src/theia.ts @@ -25,6 +25,8 @@ import checkDependencies from './check-dependencies'; import downloadPlugins from './download-plugins'; import runTest from './run-test'; import { LocalizationManager, extract } from '@theia/localization-manager'; +import { NodeRequestService } from '@theia/request/lib/node-request-service'; +import { ExtensionIdMatchesFilterFactory, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client'; const { executablePath } = require('puppeteer'); @@ -45,9 +47,7 @@ theiaCli(); function toStringArray(argv: (string | number)[]): string[]; function toStringArray(argv?: (string | number)[]): string[] | undefined; function toStringArray(argv?: (string | number)[]): string[] | undefined { - return argv === undefined - ? undefined - : argv.map(arg => String(arg)); + return argv?.map(arg => String(arg)); } function rebuildCommand(command: string, target: ApplicationProps.Target): yargs.CommandModule { apiUrl: string parallel: boolean proxyUrl?: string - proxyAuthentification?: string + proxyAuthorization?: string strictSsl: boolean + rateLimit: number + ovsxRouterConfig?: string }>({ command: 'download:plugins', describe: 'Download defined external plugins', @@ -355,17 +357,43 @@ async function theiaCli(): Promise { 'proxy-url': { describe: 'Proxy URL' }, - 'proxy-authentification': { - describe: 'Proxy authentification information' + 'proxy-authorization': { + describe: 'Proxy authorization information' }, 'strict-ssl': { describe: 'Whether to enable strict SSL mode', boolean: true, default: false + }, + 'ovsx-router-config': { + describe: 'JSON configuration file for the OVSX router client', + type: 'string' } }, - handler: async args => { - await downloadPlugins(args); + handler: async ({ apiUrl, proxyUrl, proxyAuthorization, strictSsl, ovsxRouterConfig, ...options }) => { + const requestService = new NodeRequestService(); + await requestService.configure({ + proxyUrl, + proxyAuthorization, + strictSSL: strictSsl + }); + let client: OVSXClient | undefined; + if (ovsxRouterConfig) { + const routerConfig = await fs.promises.readFile(ovsxRouterConfig, 'utf8').then(JSON.parse, error => { + console.error(error); + }); + if (routerConfig) { + client = await OVSXRouterClient.FromConfig( + routerConfig, + OVSXHttpClient.createClientFactory(requestService), + [RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory] + ); + } + } + if (!client) { + client = new OVSXHttpClient(apiUrl, requestService); + } + await downloadPlugins(client, requestService, options); }, }) .command<{ diff --git a/dev-packages/ovsx-client/README.md b/dev-packages/ovsx-client/README.md index 4e01943b0270e..055ae72421a97 100644 --- a/dev-packages/ovsx-client/README.md +++ b/dev-packages/ovsx-client/README.md @@ -16,6 +16,36 @@ The `@theia/ovsx-client` package is used to interact with `open-vsx` through its 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) diff --git a/dev-packages/ovsx-client/src/index.ts b/dev-packages/ovsx-client/src/index.ts index 391a16119e28d..8d0d7318fe398 100644 --- a/dev-packages/ovsx-client/src/index.ts +++ b/dev-packages/ovsx-client/src/index.ts @@ -14,5 +14,9 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -export * from './ovsx-client'; +export { OVSXApiFilter, OVSXApiFilterImpl } from './ovsx-api-filter'; +export { OVSXHttpClient } from './ovsx-http-client'; +export { OVSXMockClient } from './ovsx-mock-client'; +export { OVSXRouterClient, OVSXRouterConfig, OVSXRouterFilterFactory as FilterFactory } from './ovsx-router-client'; +export * from './ovsx-router-filters'; export * from './ovsx-types'; diff --git a/dev-packages/ovsx-client/src/ovsx-api-filter.ts b/dev-packages/ovsx-client/src/ovsx-api-filter.ts new file mode 100644 index 0000000000000..0c5c505491713 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-api-filter.ts @@ -0,0 +1,91 @@ +// ***************************************************************************** +// 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 { VSXAllVersions, VSXBuiltinNamespaces, VSXExtensionRaw, VSXSearchEntry } from './ovsx-types'; + +export const OVSXApiFilter = Symbol('OVSXApiFilter'); +/** + * Filter various data types based on a pre-defined supported VS Code API version. + */ +export interface OVSXApiFilter { + supportedApiVersion: string; + /** + * 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 supportedApiVersion: string + ) { } + + getLatestCompatibleExtension(extensions: VSXExtensionRaw[]): VSXExtensionRaw | undefined { + if (extensions.length === 0) { + return; + } else if (this.isBuiltinNamespace(extensions[0].namespace.toLowerCase())) { + return extensions.find(extension => this.versionGreaterThanOrEqualTo(extension.version, this.supportedApiVersion)); + } 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(allVersions.version, this.supportedApiVersion)); + } 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.lte(versionA, versionB); + } + + protected supportedVscodeApiSatisfies(vscodeApiRange: string): boolean { + return semver.satisfies(this.supportedApiVersion, vscodeApiRange); + } +} diff --git a/dev-packages/ovsx-client/src/ovsx-client.spec.ts b/dev-packages/ovsx-client/src/ovsx-client.spec.ts deleted file mode 100644 index a41a3321fa69d..0000000000000 --- a/dev-packages/ovsx-client/src/ovsx-client.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2020 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 chai from 'chai'; -import { OVSXClient } from './ovsx-client'; -import { NodeRequestService } from '@theia/request/lib/node-request-service'; -import { VSXSearchParam } from './ovsx-types'; - -const expect = chai.expect; - -describe('OVSX Client', () => { - - const apiUrl = 'https://open-vsx.org/api'; - const apiVersion = '1.40.0'; - - let client: OVSXClient; - - before(() => { - client = new OVSXClient({ - apiVersion, - apiUrl - }, new NodeRequestService()); - }); - - describe('isEngineValid', () => { - - it('should return \'true\' for a compatible engine', () => { - const a: boolean = client['isEngineSupported']('^1.20.0'); - const b: boolean = client['isEngineSupported']('^1.40.0'); - expect(a).to.eq(true); - expect(b).to.eq(true); - }); - - it('should return \'true\' for the wildcard \'*\' engine', () => { - const valid: boolean = client['isEngineSupported']('*'); - expect(valid).to.eq(true); - }); - - it('should return \'false\' for a incompatible engine', () => { - const valid: boolean = client['isEngineSupported']('^1.50.0'); - expect(valid).to.eq(false); - }); - - it('should return \'false\' for an undefined engine', () => { - const valid: boolean = client['isEngineSupported'](); - expect(valid).to.eq(false); - }); - - }); - - describe('#buildSearchUri', () => { - - it('should correctly build the search URI with the single `query` parameter present', async () => { - const expected = 'https://open-vsx.org/api/-/search?query=javascript'; - const param: VSXSearchParam = { - query: 'javascript', - }; - const query = await client['buildSearchUri'](param); - expect(query).to.eq(expected); - }); - - it('should correctly build the search URI with the multiple search parameters present', async () => { - let expected = 'https://open-vsx.org/api/-/search?query=javascript&category=languages&size=20&offset=10&includeAllVersions=true'; - let param: VSXSearchParam = { - query: 'javascript', - category: 'languages', - size: 20, - offset: 10, - includeAllVersions: true, - }; - let query = await client['buildSearchUri'](param); - expect(query).to.eq(expected); - - expected = 'https://open-vsx.org/api/-/search?query=javascript&category=languages&size=20&offset=10&sortOrder=desc&sortBy=relevance&includeAllVersions=true'; - param = { - query: 'javascript', - category: 'languages', - size: 20, - offset: 10, - sortOrder: 'desc', - sortBy: 'relevance', - includeAllVersions: true - }; - query = await client['buildSearchUri'](param); - expect(query).to.eq(expected); - }); - - }); - - describe('#isVersionLTE', () => { - - it('should determine if v1 is less than or equal to v2', () => { - expect(client['isVersionLTE']('1.40.0', '1.50.0')).equal(true, 'should be satisfied since v1 is less than v2'); - expect(client['isVersionLTE']('1.50.0', '1.50.0')).equal(true, 'should be satisfied since v1 and v2 are equal'); - expect(client['isVersionLTE']('2.0.2', '2.0.1')).equal(false, 'should not be satisfied since v1 is greater than v2'); - }); - - it('should support \'preview\' versions', () => { - expect(client['isVersionLTE']('1.40.0-next.622cb03f7e0', '1.50.0')).equal(true, 'should be satisfied since v1 is less than v2'); - expect(client['isVersionLTE']('1.50.0-next.622cb03f7e0', '1.50.0')).equal(true, 'should be satisfied since v1 and v2 are equal'); - expect(client['isVersionLTE']('1.50.0-next.622cb03f7e0', '1.50.0-next.622cb03f7e0')).equal(true, 'should be satisfied since v1 and v2 are equal'); - expect(client['isVersionLTE']('2.0.2-next.622cb03f7e0', '2.0.1')).equal(false, 'should not be satisfied since v1 is greater than v2'); - }); - - }); - -}); diff --git a/dev-packages/ovsx-client/src/ovsx-client.ts b/dev-packages/ovsx-client/src/ovsx-client.ts deleted file mode 100644 index 564e630279351..0000000000000 --- a/dev-packages/ovsx-client/src/ovsx-client.ts +++ /dev/null @@ -1,220 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 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 * as semver from 'semver'; -import { - VSXAllVersions, - VSXBuiltinNamespaces, - VSXExtensionRaw, - VSXQueryParam, - VSXQueryResult, - VSXSearchEntry, - VSXSearchParam, - VSXSearchResult -} from './ovsx-types'; -import { RequestContext, RequestService } from '@theia/request'; - -export interface OVSXClientOptions { - apiVersion: string - apiUrl: string -} - -export class OVSXClient { - - constructor(readonly options: OVSXClientOptions, protected readonly request: RequestService) { } - - async search(param?: VSXSearchParam): Promise { - const searchUri = await this.buildSearchUri(param); - try { - return await this.fetchJson(searchUri); - } catch (err) { - return { - error: err?.message || String(err), - offset: 0, - extensions: [] - }; - } - } - - protected async buildSearchUri(param?: VSXSearchParam): Promise { - return this.buildUri('api/-/search', param); - } - - protected buildQueryUri(param?: VSXQueryParam): string { - return this.buildUri('api/-/query', param); - } - - protected buildUri(url: string, param?: Object): string { - let searchUri = ''; - if (param) { - const query: string[] = []; - for (const [key, value] of Object.entries(param)) { - if (typeof value === 'string') { - query.push(`${key}=${encodeURIComponent(value)}`); - } else if (typeof value === 'boolean' || typeof value === 'number') { - query.push(`${key}=${String(value)}`); - } - } - if (query.length > 0) { - searchUri += '?' + query.join('&'); - } - } - return new URL(`${url}${searchUri}`, this.options!.apiUrl).toString(); - } - - async getExtension(id: string, queryParam?: VSXQueryParam): Promise { - const param: VSXQueryParam = { - ...queryParam, - extensionId: id - }; - const apiUri = this.buildQueryUri(param); - const result = await this.fetchJson(apiUri); - if (result.extensions && result.extensions.length > 0) { - return result.extensions[0]; - } - throw new Error(`Extension with id ${id} not found at ${apiUri}`); - } - - /** - * Get all versions of the given extension. - * @param id the requested extension id. - */ - async getAllVersions(id: string): Promise { - const param: VSXQueryParam = { - extensionId: id, - includeAllVersions: true, - }; - const apiUri = this.buildQueryUri(param); - const result = await this.fetchJson(apiUri); - if (result.extensions && result.extensions.length > 0) { - return result.extensions; - } - throw new Error(`Extension with id ${id} not found at ${apiUri}`); - } - - protected async fetchJson(url: string): Promise { - const requestContext = await this.request.request({ - url, - headers: { 'Accept': 'application/json' } - }); - return RequestContext.asJson(requestContext); - } - - async fetchText(url: string): Promise { - const requestContext = await this.request.request({ url }); - return RequestContext.asText(requestContext); - } - - /** - * 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 id the extension id. - * - * @returns the data for the latest compatible extension version if available, else `undefined`. - */ - async getLatestCompatibleExtensionVersion(id: string): Promise { - const extensions = await this.getAllVersions(id); - if (extensions.length === 0) { - return undefined; - } - - const namespace = extensions[0].namespace.toLowerCase(); - if (this.isBuiltinNamespace(namespace)) { - const apiVersion = this.options!.apiVersion; - for (const extension of extensions) { - if (this.isVersionLTE(extension.version, apiVersion)) { - return extension; - } - } - console.log(`Skipping: built-in extension "${id}" at version "${apiVersion}" does not exist.`); - } else { - for (const extension of extensions) { - if (this.isEngineSupported(extension.engines?.vscode)) { - return extension; - } - } - } - } - - /** - * Get the latest compatible version of an extension. - * @param entry the extension search entry. - * - * @returns the latest compatible version of an extension if it exists, else `undefined`. - */ - getLatestCompatibleVersion(entry: VSXSearchEntry): VSXAllVersions | undefined { - const extensions = entry.allVersions; - if (this.isBuiltinNamespace(entry.namespace)) { - const apiVersion = this.options!.apiVersion; - for (const extension of extensions) { - if (this.isVersionLTE(extension.version, apiVersion)) { - return extension; - } - } - } else { - for (const extension of extensions) { - if (this.isEngineSupported(extension.engines?.vscode)) { - return extension; - } - } - } - } - - /** - * Determine if the engine is supported by the application. - * @param engine the engine. - * - * @returns `true` if the engine satisfies the API version. - */ - protected isEngineSupported(engine?: string): boolean { - if (!engine) { - return false; - } - - // Determine engine compatibility. - if (engine === '*') { - return true; - } else { - return semver.satisfies(this.options!.apiVersion, engine); - } - } - - /** - * Determines if the extension namespace is a builtin maintained by the framework. - * @param namespace the extension namespace to verify. - */ - protected isBuiltinNamespace(namespace: string): boolean { - return namespace === VSXBuiltinNamespaces.VSCODE - || namespace === VSXBuiltinNamespaces.THEIA; - } - - /** - * Determines if the first version is less than or equal the second version. - * - v1 <= v2. - * @param a the first semver version. - * @param b the second semver version. - */ - protected isVersionLTE(a: string, b: string): boolean { - const versionA = semver.clean(a); - const versionB = semver.clean(b); - if (!versionA || !versionB) { - return false; - } - return semver.lte(versionA, versionB); - } - -} diff --git a/dev-packages/ovsx-client/src/ovsx-http-client.ts b/dev-packages/ovsx-client/src/ovsx-http-client.ts new file mode 100644 index 0000000000000..84629e61f02df --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-http-client.ts @@ -0,0 +1,85 @@ +// ***************************************************************************** +// 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'; + +export class OVSXHttpClient implements OVSXClient { + + /** + * @param requestService + * @returns factory that will cache clients based on the requested input URL. + */ + static createClientFactory(requestService: RequestService): (url: string) => OVSXClient { + // eslint-disable-next-line no-null/no-null + const cachedClients: Record = Object.create(null); + return url => cachedClients[url] ??= new this(url, requestService); + } + + constructor( + protected vsxRegistryUrl: string, + protected requestService: RequestService + ) { } + + async search(searchOptions?: VSXSearchOptions): Promise { + try { + return await this.requestJson(this.buildUrl('api/-/search', searchOptions)); + } catch (err) { + return { + error: err?.message || String(err), + offset: -1, + extensions: [] + }; + } + } + + async query(queryOptions?: VSXQueryOptions): Promise { + try { + return await this.requestJson(this.buildUrl('api/-/query', queryOptions)); + } catch (error) { + console.warn(error); + return { + extensions: [] + }; + } + } + + protected async requestJson(url: string): Promise { + return RequestContext.asJson(await this.requestService.request({ + url, + headers: { 'Accept': 'application/json' } + })); + } + + 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); + } +} diff --git a/dev-packages/ovsx-client/src/ovsx-mock-client.ts b/dev-packages/ovsx-client/src/ovsx-mock-client.ts new file mode 100644 index 0000000000000..f7366f841b62d --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-mock-client.ts @@ -0,0 +1,182 @@ +// ***************************************************************************** +// 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}` + }; + }); + return this; + } + + async query(queryOptions?: VSXQueryOptions): Promise { + return { + 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) + )) + }; + } + + async search(searchOptions?: VSXSearchOptions): Promise { + 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}`); + } + } +} diff --git a/dev-packages/ovsx-client/src/ovsx-router-client.spec-data.ts b/dev-packages/ovsx-client/src/ovsx-router-client.spec-data.ts new file mode 100644 index 0000000000000..cba56b1c3d3c3 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-router-client.spec-data.ts @@ -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 = { + [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; +}; diff --git a/dev-packages/ovsx-client/src/ovsx-router-client.spec.ts b/dev-packages/ovsx-client/src/ovsx-router-client.spec.ts new file mode 100644 index 0000000000000..c295644cf7871 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-router-client.spec.ts @@ -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 './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:/); + }); +}); diff --git a/dev-packages/ovsx-client/src/ovsx-router-client.ts b/dev-packages/ovsx-client/src/ovsx-router-client.ts new file mode 100644 index 0000000000000..a1b9bd3768e65 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-router-client.ts @@ -0,0 +1,248 @@ +// ***************************************************************************** +// 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; + filterQueryOptions?(queryOptions?: VSXQueryOptions): MaybePromise; + filterExtension?(extension: ExtensionLike): MaybePromise; +} + +/** + * @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>, remainingKeys: Set) => MaybePromise; + +/** + * 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 { + 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): Promise { + 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[] { + 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 { + 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 { + 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 { + extensions: [] + }; + } + + protected async mergedQuery(registries: string[], queryOptions?: VSXQueryOptions): Promise { + return this.mergeQueryResults(await createMapping(registries, async registry => (await this.clientProvider(registry)).query(queryOptions))); + } + + protected async mergedSearch(registries: string[], searchOptions?: VSXSearchOptions): Promise { + return this.mergeSearchResults(await createMapping(registries, async registry => (await this.clientProvider(registry)).search(searchOptions))); + } + + protected async mergeSearchResults(results: Map): Promise { + const filtering = [] as Promise[]; + 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): Promise { + const filtering = [] as Promise[]; + results.forEach((result, sourceUri) => { + result.extensions.forEach(extension => filtering.push(this.filterExtension(sourceUri, extension))); + }); + return { + extensions: removeNullValues(await Promise.all(filtering)) + }; + } + + protected async filterExtension(sourceUri: string, extension: T): Promise { + return this.runRules( + filter => filter.filterExtension?.(extension), + rule => rule.use.includes(sourceUri) ? extension : undefined, + () => extension + ); + } + + protected runRules(runFilter: (filter: OVSXRouterFilter) => unknown, onRuleMatched: (rule: OVSXRouterParsedRule) => T): Promise; + protected runRules(runFilter: (filter: OVSXRouterFilter) => unknown, onRuleMatched: (rule: OVSXRouterParsedRule) => T, onFallthru: () => U): Promise; + protected async runRules( + runFilter: (filter: OVSXRouterFilter) => unknown, + onRuleMatched: (rule: OVSXRouterParsedRule) => T, + onFallthru?: () => U + ): Promise { + 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 onFallthru?.(); + } +} + +function nonNullable(value: T | null | undefined): value is T { + // eslint-disable-next-line no-null/no-null + return typeof value !== 'undefined' && value !== null; +} + +function removeNullValues(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(values: T[], map: (value: T, index: number) => MaybePromise, thisArg?: unknown): Promise> { + 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(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; +} diff --git a/dev-packages/ovsx-client/src/ovsx-router-filters/abstract-reg-exp-filter.ts b/dev-packages/ovsx-client/src/ovsx-router-filters/abstract-reg-exp-filter.ts new file mode 100644 index 0000000000000..8e4a3a064bf32 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-router-filters/abstract-reg-exp-filter.ts @@ -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); + } +} diff --git a/dev-packages/ovsx-client/src/ovsx-router-filters/extension-id-matches-filter.ts b/dev-packages/ovsx-client/src/ovsx-router-filters/extension-id-matches-filter.ts new file mode 100644 index 0000000000000..376e1993d7716 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-router-filters/extension-id-matches-filter.ts @@ -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)); + } +} diff --git a/dev-packages/ovsx-client/src/ovsx-router-filters/index.ts b/dev-packages/ovsx-client/src/ovsx-router-filters/index.ts new file mode 100644 index 0000000000000..ee6222f3580c2 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-router-filters/index.ts @@ -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'; diff --git a/dev-packages/ovsx-client/src/ovsx-router-filters/request-contains-filter.ts b/dev-packages/ovsx-client/src/ovsx-router-filters/request-contains-filter.ts new file mode 100644 index 0000000000000..d08643ce40115 --- /dev/null +++ b/dev-packages/ovsx-client/src/ovsx-router-filters/request-contains-filter.ts @@ -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); + } +} diff --git a/dev-packages/ovsx-client/src/ovsx-types.ts b/dev-packages/ovsx-client/src/ovsx-types.ts index cf14e519a953f..1c0c2a197c252 100644 --- a/dev-packages/ovsx-client/src/ovsx-types.ts +++ b/dev-packages/ovsx-client/src/ovsx-types.ts @@ -14,14 +14,58 @@ // 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(extension: T): `${string}.${string}` { + return `${extension.namespace}.${extension.name}`; + } + export function idWithVersion(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; + /** + * GET https://openvsx.org/api/-/query + * + * Fetch one or all versions of an extension. + */ + query(queryOptions?: VSXQueryOptions): Promise; +} + +/** @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/#/registry-api/searchUsingGET. + * 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 VSXSearchParam { +export interface VSXSearchOptions { /** * The query text for searching. */ @@ -47,7 +91,12 @@ export interface VSXSearchParam { */ sortBy?: 'averageRating' | 'downloadCount' | 'relevance' | 'timestamp'; /** - * Determines whether to include information regarding all available entries for the entry. + * 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; } @@ -56,40 +105,72 @@ export interface VSXSearchParam { * Should be aligned with https://github.com/eclipse/openvsx/blob/e8f64fe145fc05d2de1469735d50a7a90e400bc4/server/src/main/java/org/eclipse/openvsx/json/SearchResultJson.java */ export interface VSXSearchResult { - readonly error?: string; - readonly offset: number; - readonly extensions: VSXSearchEntry[]; + error?: string; + offset: number; + extensions: VSXSearchEntry[]; +} + +/** @deprecated since 1.31.0 use {@link VSXQueryOptions} instead */ +export type VSXQueryParam = VSXQueryOptions; +/** + * 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; +} + +export interface VSXQueryResult { + 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, + url: string; + version: string; engines?: { - [version: string]: string - } + [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 { - readonly url: string; - readonly files: { - download: string - manifest?: string - readme?: string - license?: string - icon?: string - } - readonly name: string; - readonly namespace: string; - readonly version: string; - readonly timestamp: string; - readonly averageRating?: number; - readonly downloadCount: number; - readonly displayName?: string; - readonly description?: string; - readonly allVersions: VSXAllVersions[]; + 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'; @@ -98,69 +179,57 @@ 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 + loginName: string; + homepage?: string; } export interface VSXExtensionRawFiles { - download: string - readme?: string - license?: string - icon?: string + 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 { - readonly error?: string; - readonly namespaceUrl: string; - readonly reviewsUrl: string; - readonly name: string; - readonly namespace: string; - readonly publishedBy: VSXUser - readonly namespaceAccess: VSXExtensionNamespaceAccess; - readonly files: VSXExtensionRawFiles, - readonly allVersions: { - [version: string]: string - } - readonly averageRating?: number; - readonly downloadCount: number; - readonly reviewCount: number; - readonly version: string; - readonly timestamp: string; - readonly preview?: boolean; - readonly displayName?: string; - readonly description?: string; - readonly categories?: string[]; - readonly tags?: string[]; - readonly license?: string; - readonly homepage?: string; - readonly repository?: string; - readonly bugs?: string; - readonly markdown?: string; - readonly galleryColor?: string; - readonly galleryTheme?: string; - readonly qna?: string; - readonly engines?: { [engine: string]: string }; -} - -export interface VSXQueryParam { - namespaceName?: string; - extensionName?: string; - extensionVersion?: string; - extensionId?: string; - extensionUuid?: string; - namespaceUuid?: string; - includeAllVersions?: boolean; -} - -export interface VSXQueryResult { - extensions?: VSXExtensionRaw[]; + error?: string; + namespaceUrl: string; + reviewsUrl: string; + name: string; + namespace: string; + publishedBy: VSXUser + namespaceAccess: VSXExtensionNamespaceAccess; + files: VSXExtensionRawFiles; + allVersions: { + [version: string]: string; + }; + averageRating?: number; + downloadCount: number; + reviewCount: number; + version: string; + timestamp: string; + preview?: boolean; + displayName?: string; + description?: string; + categories?: string[]; + tags?: string[]; + license?: string; + homepage?: string; + repository?: string; + bugs?: string; + markdown?: string; + galleryColor?: string; + galleryTheme?: string; + qna?: string; + engines?: { + [engine: string]: string; + }; } export interface VSXResponseError extends Error { - statusCode: number + statusCode: number; } export namespace VSXResponseError { @@ -173,13 +242,26 @@ export namespace VSXResponseError { * 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; diff --git a/dev-packages/ovsx-client/src/types.ts b/dev-packages/ovsx-client/src/types.ts new file mode 100644 index 0000000000000..e967366a50eb9 --- /dev/null +++ b/dev-packages/ovsx-client/src/types.ts @@ -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 | PromiseLike; diff --git a/examples/api-samples/README.md b/examples/api-samples/README.md index 15f039c9bda34..4ac5674d95695 100644 --- a/examples/api-samples/README.md +++ b/examples/api-samples/README.md @@ -19,6 +19,12 @@ The purpose of the extension is to: The extension is for reference and test purposes only and is not published on `npm` (`private: true`). +### Sample mock OpenVSX server + +These samples contain a mock implementation of an OpenVSX server. This is done +for testing purposes only. It is currently hosted at +`/mock-open-vsx/api/...`. + ## Additional Information - [Theia - GitHub](https://github.com/eclipse-theia/theia) diff --git a/examples/api-samples/package.json b/examples/api-samples/package.json index b28e1792c41e1..ae2bef15eb3b5 100644 --- a/examples/api-samples/package.json +++ b/examples/api-samples/package.json @@ -10,6 +10,7 @@ "@theia/monaco": "1.38.0", "@theia/monaco-editor-core": "1.72.3", "@theia/output": "1.38.0", + "@theia/ovsx-client": "1.38.0", "@theia/search-in-workspace": "1.38.0", "@theia/toolbar": "1.38.0", "@theia/vsx-registry": "1.38.0", diff --git a/examples/api-samples/src/browser/api-samples-frontend-module.ts b/examples/api-samples/src/browser/api-samples-frontend-module.ts index 2d168ce4f1146..a2e36fbcaa36f 100644 --- a/examples/api-samples/src/browser/api-samples-frontend-module.ts +++ b/examples/api-samples/src/browser/api-samples-frontend-module.ts @@ -26,6 +26,8 @@ import { bindSampleToolbarContribution } from './toolbar/sample-toolbar-contribu import '../../src/browser/style/branding.css'; import { bindMonacoPreferenceExtractor } from './monaco-editor-preferences/monaco-editor-preference-extractor'; +import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory'; +import { bindSampleAppInfo } from './vsx/sample-frontend-app-info'; export default new ContainerModule(( bind: interfaces.Bind, @@ -42,4 +44,6 @@ export default new ContainerModule(( bindSampleFilteredCommandContribution(bind); bindSampleToolbarContribution(bind, rebind); bindMonacoPreferenceExtractor(bind); + bindSampleAppInfo(bind); + rebindOVSXClientFactory(rebind); }); diff --git a/examples/api-samples/src/browser/vsx/sample-frontend-app-info.ts b/examples/api-samples/src/browser/vsx/sample-frontend-app-info.ts new file mode 100644 index 0000000000000..a48e539113e7c --- /dev/null +++ b/examples/api-samples/src/browser/vsx/sample-frontend-app-info.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// 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 { Endpoint } from '@theia/core/lib/browser'; +import { injectable, interfaces } from '@theia/core/shared/inversify'; +import { SampleAppInfo } from '../../common/vsx/sample-app-info'; + +@injectable() +export class SampleFrontendAppInfo implements SampleAppInfo { + + async getSelfOrigin(): Promise { + return new Endpoint().origin; + } +} + +export function bindSampleAppInfo(bind: interfaces.Bind): void { + bind(SampleAppInfo).to(SampleFrontendAppInfo).inSingletonScope(); +} diff --git a/examples/api-samples/src/common/vsx/sample-app-info.ts b/examples/api-samples/src/common/vsx/sample-app-info.ts new file mode 100644 index 0000000000000..b1ca59915a6a0 --- /dev/null +++ b/examples/api-samples/src/common/vsx/sample-app-info.ts @@ -0,0 +1,22 @@ +// ***************************************************************************** +// 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 { interfaces } from '@theia/core/shared/inversify'; + +export const SampleAppInfo = Symbol('SampleAppInfo') as symbol & interfaces.Abstract; +export interface SampleAppInfo { + getSelfOrigin(): Promise; +} diff --git a/examples/api-samples/src/common/vsx/sample-ovsx-client-factory.ts b/examples/api-samples/src/common/vsx/sample-ovsx-client-factory.ts new file mode 100644 index 0000000000000..257cd1e46e939 --- /dev/null +++ b/examples/api-samples/src/common/vsx/sample-ovsx-client-factory.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// 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 { interfaces } from '@theia/core/shared/inversify'; +import { OVSXUrlResolver } from '@theia/vsx-registry/lib/common'; +import { SampleAppInfo } from './sample-app-info'; + +export function rebindOVSXClientFactory(rebind: interfaces.Rebind): void { + // rebind the OVSX client factory so that we can replace patterns like "${self}" in the configs: + rebind(OVSXUrlResolver) + .toDynamicValue(ctx => { + const appInfo = ctx.container.get(SampleAppInfo); + const selfOrigin = appInfo.getSelfOrigin(); + return async url => url.replace('${self}', await selfOrigin); + }) + .inSingletonScope(); +} diff --git a/examples/api-samples/src/node/api-samples-backend-module.ts b/examples/api-samples/src/node/api-samples-backend-module.ts index e3c5ca963926b..e86009f12868f 100644 --- a/examples/api-samples/src/node/api-samples-backend-module.ts +++ b/examples/api-samples/src/node/api-samples-backend-module.ts @@ -15,10 +15,20 @@ // ***************************************************************************** import { ContainerModule } from '@theia/core/shared/inversify'; -import { BackendApplicationServer } from '@theia/core/lib/node'; +import { BackendApplicationContribution, BackendApplicationServer } from '@theia/core/lib/node'; import { SampleBackendApplicationServer } from './sample-backend-application-server'; +import { SampleMockOpenVsxServer } from './sample-mock-open-vsx-server'; +import { SampleAppInfo } from '../common/vsx/sample-app-info'; +import { SampleBackendAppInfo } from './sample-backend-app-info'; +import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory'; -export default new ContainerModule(bind => { +export default new ContainerModule((bind, unbind, isBound, rebind) => { + rebindOVSXClientFactory(rebind); + bind(SampleBackendAppInfo).toSelf().inSingletonScope(); + bind(SampleAppInfo).toService(SampleBackendAppInfo); + bind(BackendApplicationContribution).toService(SampleBackendAppInfo); + // bind a mock/sample OpenVSX registry: + bind(BackendApplicationContribution).to(SampleMockOpenVsxServer).inSingletonScope(); if (process.env.SAMPLE_BACKEND_APPLICATION_SERVER) { bind(BackendApplicationServer).to(SampleBackendApplicationServer).inSingletonScope(); } diff --git a/examples/api-samples/src/node/sample-backend-app-info.ts b/examples/api-samples/src/node/sample-backend-app-info.ts new file mode 100644 index 0000000000000..dbc7b3c7e6b55 --- /dev/null +++ b/examples/api-samples/src/node/sample-backend-app-info.ts @@ -0,0 +1,53 @@ +// ***************************************************************************** +// 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 { environment } from '@theia/core/lib/common'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { BackendApplicationCliContribution, BackendApplicationContribution } from '@theia/core/lib/node'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import * as net from 'net'; +import { SampleAppInfo } from '../common/vsx/sample-app-info'; + +@injectable() +export class SampleBackendAppInfo implements SampleAppInfo, BackendApplicationContribution { + + protected addressDeferred = new Deferred(); + + @inject(BackendApplicationCliContribution) + protected backendCli: BackendApplicationCliContribution; + + onStart(server: net.Server): void { + const address = server.address(); + // eslint-disable-next-line no-null/no-null + if (typeof address === 'object' && address !== null) { + this.addressDeferred.resolve(address); + } else { + this.addressDeferred.resolve({ + address: '127.0.0.1', + port: 3000, + family: '4' + }); + } + } + + async getSelfOrigin(): Promise { + const { ssl } = this.backendCli; + const protocol = ssl ? 'https' : 'http'; + const { address, port } = await this.addressDeferred.promise; + const hostname = environment.electron.is() ? 'localhost' : address; + return `${protocol}://${hostname}:${port}`; + } +} diff --git a/examples/api-samples/src/node/sample-mock-open-vsx-server.ts b/examples/api-samples/src/node/sample-mock-open-vsx-server.ts new file mode 100644 index 0000000000000..f2f1fdc1bf465 --- /dev/null +++ b/examples/api-samples/src/node/sample-mock-open-vsx-server.ts @@ -0,0 +1,161 @@ +// ***************************************************************************** +// 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 { BackendApplicationContribution } from '@theia/core/lib/node'; +import * as express from '@theia/core/shared/express'; +import * as fs from 'fs'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OVSXMockClient, VSXExtensionRaw } from '@theia/ovsx-client'; +import * as path from 'path'; +import { SampleAppInfo } from '../common/vsx/sample-app-info'; + +type VersionedId = `${string}.${string}@${string}`; + +/** + * This class implements a very crude OpenVSX mock server for testing. + * + * See {@link configure}'s implementation for supported REST APIs. + */ +@injectable() +export class SampleMockOpenVsxServer implements BackendApplicationContribution { + + @inject(SampleAppInfo) + protected appInfo: SampleAppInfo; + + get mockServerPath(): string { + return '/mock-open-vsx'; + } + + get pluginsDbPath(): string { + return '../../sample-plugins'; + } + + async configure(app: express.Application): Promise { + const selfOrigin = await this.appInfo.getSelfOrigin(); + const baseUrl = `${selfOrigin}${this.mockServerPath}`; + const pluginsDb = await this.findMockPlugins(this.pluginsDbPath, baseUrl); + const staticFileHandlers = new Map(Array.from(pluginsDb.entries(), ([key, value]) => [key, express.static(value.path)])); + const mockClient = new OVSXMockClient(Array.from(pluginsDb.values(), value => value.data)); + app.use( + this.mockServerPath + '/api', + express.Router() + .get('/-/query', async (req, res) => { + res.json(await mockClient.query(this.sanitizeQuery(req.query))); + }) + .get('/-/search', async (req, res) => { + res.json(await mockClient.search(this.sanitizeQuery(req.query))); + }) + .get('/:namespace', async (req, res) => { + const extensions = mockClient.extensions + .filter(ext => req.params.namespace === ext.namespace) + .map(ext => `${ext.namespaceUrl}/${ext.name}`); + if (extensions.length === 0) { + res.status(404).json({ error: `Namespace not found: ${req.params.namespace}` }); + } else { + res.json({ + name: req.params.namespace, + extensions + }); + } + }) + .get('/:namespace/:name', async (req, res) => { + res.json(mockClient.extensions.find(ext => req.params.namespace === ext.namespace && req.params.name === ext.name)); + }) + .get('/:namespace/:name/reviews', async (req, res) => { + res.json([]); + }) + // implicitly GET/HEAD because of the express.static handlers + .use('/:namespace/:name/:version/file', async (req, res, next) => { + const versionedId = this.getVersionedId(req.params.namespace, req.params.name, req.params.version); + const staticFileHandler = staticFileHandlers.get(versionedId); + if (!staticFileHandler) { + return next(); + } + staticFileHandler(req, res, next); + }) + ); + } + + protected getVersionedId(namespace: string, name: string, version: string): VersionedId { + return `${namespace}.${name}@${version}`; + } + + protected sanitizeQuery(query?: Record): Record { + return typeof query === 'object' + ? Object.fromEntries(Object.entries(query).filter(([key, value]) => typeof value === 'string') as [string, string][]) + : {}; + } + + /** + * This method expects the following folder hierarchy: `pluginsDbPath/namespace/pluginName/pluginFiles...` + * @param pluginsDbPath where to look for plugins on the disk. + * @param baseUrl used when generating the URLs for {@link VSXExtensionRaw} properties. + */ + protected async findMockPlugins(pluginsDbPath: string, baseUrl: string): Promise> { + const url = new OVSXMockClient.UrlBuilder(baseUrl); + const result = new Map(); + if (!await this.isDirectory(pluginsDbPath)) { + console.error(`ERROR: ${pluginsDbPath} is not a directory!`); + return result; + } + const namespaces = await fs.promises.readdir(pluginsDbPath); + await Promise.all(namespaces.map(async namespace => { + const namespacePath = path.join(pluginsDbPath, namespace); + if (!await this.isDirectory(namespacePath)) { + return; + } + const names = await fs.promises.readdir(namespacePath); + await Promise.all(names.map(async pluginName => { + const pluginPath = path.join(namespacePath, pluginName); + if (!await this.isDirectory(pluginPath)) { + return; + } + const packageJsonPath = path.join(pluginPath, 'package.json'); + const { name, version } = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8')); + const versionedId = this.getVersionedId(namespace, name, version); + result.set(versionedId, { + path: pluginPath, + data: { + allVersions: {}, + downloadCount: 0, + files: { + // the default generated name from vsce is NAME-VERSION.vsix + download: url.extensionFileUrl(namespace, name, version, `/${name}-${version}.vsix`), + icon: url.extensionFileUrl(namespace, name, version, '/icon128.png'), + readme: url.extensionFileUrl(namespace, name, version, '/README.md') + }, + name, + namespace, + namespaceAccess: 'public', + namespaceUrl: url.namespaceUrl(namespace), + publishedBy: { + loginName: 'mock-open-vsx' + }, + reviewCount: 0, + reviewsUrl: url.extensionReviewsUrl(namespace, name), + timestamp: new Date().toISOString(), + version, + } + }); + })); + })); + return result; + } + + protected async isDirectory(fsPath: string): Promise { + return (await fs.promises.stat(fsPath)).isDirectory(); + } +} diff --git a/examples/api-samples/tsconfig.json b/examples/api-samples/tsconfig.json index 70c39bcfd947d..515744a9cd596 100644 --- a/examples/api-samples/tsconfig.json +++ b/examples/api-samples/tsconfig.json @@ -9,6 +9,9 @@ "src" ], "references": [ + { + "path": "../../dev-packages/ovsx-client" + }, { "path": "../../packages/core" }, diff --git a/examples/browser/package.json b/examples/browser/package.json index 0bbb14fbf2070..47f7b40234119 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -67,7 +67,7 @@ "coverage:clean": "rimraf .nyc_output && rimraf coverage", "coverage:report": "nyc report --reporter=html", "rebuild": "theia rebuild:browser --cacheRoot ../..", - "start": "theia start --plugins=local-dir:../../plugins", + "start": "yarn -s rebuild && theia start --plugins=local-dir:../../plugins --ovsx-router-config=../ovsx-router-config.json", "start:debug": "yarn -s start --log-level=debug", "start:watch": "concurrently --kill-others -n tsc,bundle,run -c red,yellow,green \"tsc -b -w --preserveWatchOutput\" \"yarn -s watch:bundle\" \"yarn -s start\"", "test": "theia test . --plugins=local-dir:../../plugins --test-spec=../api-tests/**/*.spec.js", diff --git a/examples/electron/package.json b/examples/electron/package.json index 88561b84cbdaf..9cb0190e552d4 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -66,7 +66,7 @@ "compile": "tsc -b", "lint": "theiaext lint", "rebuild": "theia rebuild:electron --cacheRoot ../..", - "start": "theia start --plugins=local-dir:../../plugins", + "start": "yarn -s rebuild && theia start --plugins=local-dir:../../plugins --ovsx-router-config=../ovsx-router-config.json", "start:debug": "yarn -s start --log-level=debug --remote-debugging-port=9222", "start:watch": "concurrently --kill-others -n tsc,bundle,run -c red,yellow,green \"tsc -b -w --preserveWatchOutput\" \"yarn -s watch:bundle\" \"yarn -s start\"", "test": "electron-mocha --timeout 60000 \"./lib/test/**/*.espec.js\"", diff --git a/examples/ovsx-router-config.json b/examples/ovsx-router-config.json new file mode 100644 index 0000000000000..da597b1a7bad4 --- /dev/null +++ b/examples/ovsx-router-config.json @@ -0,0 +1,16 @@ +{ + "registries": { + "public": "https://open-vsx.org/", + "mock": "${self}/mock-open-vsx/" + }, + "use": [ + "mock", + "public" + ], + "rules": [ + { + "ifRequestContains": "\\bsample-namespace\\b", + "use": "mock" + } + ] +} diff --git a/package.json b/package.json index 9d3fdb00fb7a9..dbda4a4b56a5d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/eslint-plugin-tslint": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", + "@vscode/vsce": "^2.15.0", "chai": "4.3.4", "chai-spies": "1.0.0", "chai-string": "^1.4.0", @@ -94,7 +95,8 @@ "workspaces": [ "dev-packages/*", "examples/*", - "packages/*" + "packages/*", + "sample-plugins/*/*" ], "theiaPluginsDir": "plugins", "theiaPlugins": { diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 52de2157cf538..65d2741d21216 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -43,8 +43,7 @@ export * from './resource'; export * from './selection'; export * from './selection-service'; export * from './strings'; +export * from './telemetry'; export * from './types'; export { default as URI } from './uri'; export * from './view-column'; -export * from './telemetry'; - diff --git a/packages/core/src/node/backend-application.ts b/packages/core/src/node/backend-application.ts index 5e1e9f97f0ab1..1f68370959219 100644 --- a/packages/core/src/node/backend-application.ts +++ b/packages/core/src/node/backend-application.ts @@ -47,7 +47,12 @@ export interface BackendApplicationServer extends BackendApplicationContribution export const BackendApplicationContribution = Symbol('BackendApplicationContribution'); /** - * Contribution for hooking into the backend lifecycle. + * Contribution for hooking into the backend lifecycle: + * + * - `initialize()` + * - `configure(expressApp)` + * - `onStart(httpServer)` + * - `onStop()` */ export interface BackendApplicationContribution { /** @@ -134,7 +139,6 @@ export class BackendApplicationCliContribution implements CliContribution { } return process.cwd(); } - } /** diff --git a/packages/vsx-registry/README.md b/packages/vsx-registry/README.md index 19f0d2a1edc5e..4789962d545ee 100644 --- a/packages/vsx-registry/README.md +++ b/packages/vsx-registry/README.md @@ -20,6 +20,15 @@ The extension connects to the public Open VSX Registry hosted on `http://open-vs One can host own instance of a [registry](https://github.com/eclipse/openvsx#eclipse-open-vsx) and configure `VSX_REGISTRY_URL` environment variable to use it. +### Using multiple registries + +It is possible to target multiple registries by specifying a CLI argument when +running the backend: `--ovsx-router-config=` where `path` must point to +a json defining an `OVSXRouterConfig` object. + +See `@theia/ovsx-client`'s documentation to read more about `OVSXRouterClient` +and its `OVSXRouterConfig` configuration. + ## Additional Information - [API documentation for `@theia/vsx-registry`](https://eclipse-theia.github.io/theia/docs/next/modules/vsx-registry.html) diff --git a/packages/vsx-registry/package.json b/packages/vsx-registry/package.json index cb22c29ddf3a9..20b9ba623db84 100644 --- a/packages/vsx-registry/package.json +++ b/packages/vsx-registry/package.json @@ -22,6 +22,10 @@ "theia-extension" ], "theiaExtensions": [ + { + "frontend": "lib/common/vsx-registry-common-module", + "backend": "lib/common/vsx-registry-common-module" + }, { "frontend": "lib/browser/vsx-registry-frontend-module", "backend": "lib/node/vsx-registry-backend-module" diff --git a/packages/vsx-registry/src/browser/vsx-extension.tsx b/packages/vsx-registry/src/browser/vsx-extension.tsx index 2994fe6b25618..3e2c106c38d7f 100644 --- a/packages/vsx-registry/src/browser/vsx-extension.tsx +++ b/packages/vsx-registry/src/browser/vsx-extension.tsx @@ -16,7 +16,7 @@ import * as React from '@theia/core/shared/react'; import * as DOMPurify from '@theia/core/shared/dompurify'; -import { injectable, inject } from '@theia/core/shared/inversify'; +import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { TreeElement, TreeElementNode } from '@theia/core/lib/browser/source-tree'; import { OpenerService, open, OpenerOptions } from '@theia/core/lib/browser/opener-service'; @@ -135,6 +135,13 @@ export class VSXExtension implements VSXExtensionData, TreeElement { protected readonly data: Partial = {}; + protected registryUri: Promise; + + @postConstruct() + protected postConstruct(): void { + this.registryUri = this.environment.getRegistryUri(); + } + get uri(): URI { return VSCodeExtensionUri.toUri(this.id); } @@ -333,8 +340,14 @@ export class VSXExtension implements VSXExtensionData, TreeElement { * @returns the registry link for the given extension at the path. */ async getRegistryLink(path = ''): Promise { - const uri = new URI(await this.environment.getRegistryUri()); - return uri.resolve('extension/' + this.id.replace('.', '/')).resolve(path); + const registryUri = new URI(await this.registryUri); + if (this.downloadUrl) { + const downloadUri = new URI(this.downloadUrl); + if (downloadUri.authority !== registryUri.authority) { + throw new Error('cannot generate a valid URL'); + } + } + return registryUri.resolve('extension/' + this.id.replace('.', '/')).resolve(path); } async serialize(): Promise { diff --git a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts index 88a131fd35cf1..80e394562a6b9 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts +++ b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts @@ -34,22 +34,22 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { BUILTIN_QUERY, INSTALLED_QUERY, RECOMMENDED_QUERY } from './vsx-extensions-search-model'; import { IGNORE_RECOMMENDATIONS_ID } from './recommended-extensions/recommended-extensions-preference-contribution'; import { VSXExtensionsCommands } from './vsx-extension-commands'; -import { VSXExtensionRaw } from '@theia/ovsx-client'; +import { VSXExtensionRaw, OVSXApiFilter } from '@theia/ovsx-client'; import { OVSXClientProvider } from '../common/ovsx-client-provider'; @injectable() -export class VSXExtensionsContribution extends AbstractViewContribution - implements ColorContribution, FrontendApplicationContribution { +export class VSXExtensionsContribution extends AbstractViewContribution implements ColorContribution, FrontendApplicationContribution { - @inject(VSXExtensionsModel) protected readonly model: VSXExtensionsModel; - @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; - @inject(FileDialogService) protected readonly fileDialogService: FileDialogService; - @inject(MessageService) protected readonly messageService: MessageService; - @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - @inject(ClipboardService) protected readonly clipboardService: ClipboardService; - @inject(PreferenceService) protected readonly preferenceService: PreferenceService; - @inject(OVSXClientProvider) protected readonly clientProvider: OVSXClientProvider; - @inject(QuickInputService) protected readonly quickInput: QuickInputService; + @inject(VSXExtensionsModel) protected model: VSXExtensionsModel; + @inject(CommandRegistry) protected commandRegistry: CommandRegistry; + @inject(FileDialogService) protected fileDialogService: FileDialogService; + @inject(MessageService) protected messageService: MessageService; + @inject(LabelProvider) protected labelProvider: LabelProvider; + @inject(ClipboardService) protected clipboardService: ClipboardService; + @inject(PreferenceService) protected preferenceService: PreferenceService; + @inject(OVSXClientProvider) protected clientProvider: OVSXClientProvider; + @inject(OVSXApiFilter) protected vsxApiFilter: OVSXApiFilter; + @inject(QuickInputService) protected quickInput: QuickInputService; constructor() { super({ @@ -209,8 +209,8 @@ export class VSXExtensionsContribution extends AbstractViewContribution; + /** + * Single source for all extensions + */ + protected readonly extensions = new Map(); protected readonly onDidChangeEmitter = new Emitter(); - readonly onDidChange = this.onDidChangeEmitter.event; + protected _installed = new Set(); + protected _recommended = new Set(); + protected _searchResult = new Set(); + protected _searchError?: string; + + protected searchCancellationTokenSource = new CancellationTokenSource(); + protected updateSearchResult = debounce(async () => { + const { token } = this.resetSearchCancellationTokenSource(); + await this.doUpdateSearchResult({ query: this.search.query, includeAllVersions: true }, token); + }, 500); @inject(OVSXClientProvider) protected clientProvider: OVSXClientProvider; @@ -59,11 +74,15 @@ export class VSXExtensionsModel { @inject(VSXExtensionsSearchModel) readonly search: VSXExtensionsSearchModel; - protected readonly initialized = new Deferred(); + @inject(RequestService) + protected request: RequestService; + + @inject(OVSXApiFilter) + protected vsxApiFilter: OVSXApiFilter; @postConstruct() protected init(): void { - this.doInit(); + this.initialized = this.doInit().catch(console.error); } protected async doInit(): Promise { @@ -72,7 +91,56 @@ export class VSXExtensionsModel { this.initSearchResult(), this.initRecommended(), ]); - this.initialized.resolve(); + } + + get onDidChange(): Event { + return this.onDidChangeEmitter.event; + } + + get installed(): IterableIterator { + return this._installed.values(); + } + + get searchError(): string | undefined { + return this._searchError; + } + + get searchResult(): IterableIterator { + return this._searchResult.values(); + } + + get recommended(): IterableIterator { + return this._recommended.values(); + } + + isInstalled(id: string): boolean { + return this._installed.has(id); + } + + getExtension(id: string): VSXExtension | undefined { + return this.extensions.get(id); + } + + resolve(id: string): Promise { + return this.doChange(async () => { + await this.initialized; + const extension = await this.refresh(id); + if (!extension) { + throw new Error(`Failed to resolve ${id} extension.`); + } + if (extension.readmeUrl) { + try { + const rawReadme = RequestContext.asText(await this.request.request({ url: extension.readmeUrl })); + const readme = this.compileReadme(rawReadme); + extension.update({ readme }); + } catch (e) { + if (!VSXResponseError.is(e) || e.statusCode !== 404) { + console.error(`[${id}]: failed to compile readme, reason:`, e); + } + } + } + return extension; + }); } protected async initInstalled(): Promise { @@ -108,37 +176,9 @@ export class VSXExtensionsModel { } } - /** - * single source of all extensions - */ - protected readonly extensions = new Map(); - - protected _installed = new Set(); - get installed(): IterableIterator { - return this._installed.values(); - } - - isInstalled(id: string): boolean { - return this._installed.has(id); - } - - protected _searchError?: string; - get searchError(): string | undefined { - return this._searchError; - } - - protected _searchResult = new Set(); - get searchResult(): IterableIterator { - return this._searchResult.values(); - } - - protected _recommended = new Set(); - get recommended(): IterableIterator { - return this._recommended.values(); - } - - getExtension(id: string): VSXExtension | undefined { - return this.extensions.get(id); + protected resetSearchCancellationTokenSource(): CancellationTokenSource { + this.searchCancellationTokenSource.cancel(); + return this.searchCancellationTokenSource = new CancellationTokenSource(); } protected setExtension(id: string): VSXExtension { @@ -155,25 +195,18 @@ export class VSXExtensionsModel { protected doChange(task: () => Promise, token: CancellationToken = CancellationToken.None): Promise { return this.progressService.withProgress('', 'extensions', async () => { if (token && token.isCancellationRequested) { - return undefined; + return; } const result = await task(); if (token && token.isCancellationRequested) { - return undefined; + return; } - this.onDidChangeEmitter.fire(undefined); + this.onDidChangeEmitter.fire(); return result; }); } - protected searchCancellationTokenSource = new CancellationTokenSource(); - protected updateSearchResult = debounce(() => { - this.searchCancellationTokenSource.cancel(); - this.searchCancellationTokenSource = new CancellationTokenSource(); - const query = this.search.query; - return this.doUpdateSearchResult({ query, includeAllVersions: true }, this.searchCancellationTokenSource.token); - }, 500); - protected doUpdateSearchResult(param: VSXSearchParam, token: CancellationToken): Promise { + protected doUpdateSearchResult(param: VSXSearchOptions, token: CancellationToken): Promise { return this.doChange(async () => { const searchResult = new Set(); if (!param.query) { @@ -188,8 +221,8 @@ export class VSXExtensionsModel { } for (const data of result.extensions) { const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase(); - const extension = client.getLatestCompatibleVersion(data); - if (!extension) { + const allVersions = this.vsxApiFilter.getLatestCompatibleVersion(data); + if (!allVersions) { continue; } this.setExtension(id).update(Object.assign(data, { @@ -198,7 +231,7 @@ export class VSXExtensionsModel { iconUrl: data.files.icon, readmeUrl: data.files.readme, licenseUrl: data.files.license, - version: extension.version + version: allVersions.version })); searchResult.add(id); } @@ -268,29 +301,6 @@ export class VSXExtensionsModel { }; } - resolve(id: string): Promise { - return this.doChange(async () => { - await this.initialized.promise; - const extension = await this.refresh(id); - if (!extension) { - throw new Error(`Failed to resolve ${id} extension.`); - } - if (extension.readmeUrl) { - try { - const client = await this.clientProvider(); - const rawReadme = await client.fetchText(extension.readmeUrl); - const readme = this.compileReadme(rawReadme); - extension.update({ readme }); - } catch (e) { - if (!VSXResponseError.is(e) || e.statusCode !== 404) { - console.error(`[${id}]: failed to compile readme, reason:`, e); - } - } - } - return extension; - }); - } - protected compileReadme(readmeMarkdown: string): string { const readmeHtml = markdownit({ html: true }).render(readmeMarkdown); return DOMPurify.sanitize(readmeHtml); @@ -303,9 +313,18 @@ export class VSXExtensionsModel { return extension; } const client = await this.clientProvider(); - const data = version !== undefined - ? await client.getExtension(id, { extensionVersion: version, includeAllVersions: true }) - : await client.getLatestCompatibleExtensionVersion(id); + let data: VSXExtensionRaw | undefined; + if (version === undefined) { + const { extensions } = await client.query({ extensionId: id }); + if (extensions?.length) { + data = this.vsxApiFilter.getLatestCompatibleExtension(extensions); + } + } else { + const { extensions } = await client.query({ extensionId: id, extensionVersion: version, includeAllVersions: true }); + if (extensions?.length) { + data = extensions?.[0]; + } + } if (!data) { return; } diff --git a/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts index 649aacc6b835a..f3ebbccd2e211 100644 --- a/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts +++ b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts @@ -33,21 +33,14 @@ import { VSXExtensionsSourceOptions } from './vsx-extensions-source'; import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; import { bindExtensionPreferences } from './recommended-extensions/recommended-extensions-preference-contribution'; import { bindPreferenceProviderOverrides } from './recommended-extensions/preference-provider-overrides'; -import { OVSXClientProvider, createOVSXClient } from '../common/ovsx-client-provider'; import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment'; -import { RequestService } from '@theia/core/shared/@theia/request'; import { LanguageQuickPickService } from '@theia/core/lib/browser/i18n/language-quick-pick-service'; import { VSXLanguageQuickPickService } from './vsx-language-quick-pick-service'; -export default new ContainerModule((bind, unbind, _, rebind) => { - bind(OVSXClientProvider).toDynamicValue(ctx => { - const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment), ctx.container.get(RequestService)); - return () => clientPromise; - }).inSingletonScope(); - bind(VSXEnvironment).toDynamicValue( - ctx => WebSocketConnectionProvider.createProxy(ctx.container, VSX_ENVIRONMENT_PATH) - ).inSingletonScope(); - +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(VSXEnvironment) + .toDynamicValue(ctx => WebSocketConnectionProvider.createProxy(ctx.container, VSX_ENVIRONMENT_PATH)) + .inSingletonScope(); bind(VSXExtension).toSelf(); bind(VSXExtensionFactory).toFactory(ctx => (option: VSXExtensionOptions) => { const child = ctx.container.createChild(); diff --git a/packages/vsx-registry/src/common/index.ts b/packages/vsx-registry/src/common/index.ts new file mode 100644 index 0000000000000..960beb3e17f4f --- /dev/null +++ b/packages/vsx-registry/src/common/index.ts @@ -0,0 +1,19 @@ +// ***************************************************************************** +// 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 { OVSXClientProvider, OVSXUrlResolver } from './ovsx-client-provider'; +export { VSXEnvironment } from './vsx-environment'; +export { VSXExtensionUri } from './vsx-extension-uri'; diff --git a/packages/vsx-registry/src/common/ovsx-client-provider.ts b/packages/vsx-registry/src/common/ovsx-client-provider.ts index 33de3e1cfc2d2..5b6613b47272b 100644 --- a/packages/vsx-registry/src/common/ovsx-client-provider.ts +++ b/packages/vsx-registry/src/common/ovsx-client-provider.ts @@ -14,17 +14,22 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { MaybePromise } from '@theia/core/lib/common'; import { RequestService } from '@theia/core/shared/@theia/request'; -import { OVSXClient } from '@theia/ovsx-client'; +import type { interfaces } from '@theia/core/shared/inversify'; +import { OVSXClient, OVSXHttpClient } from '@theia/ovsx-client'; import { VSXEnvironment } from './vsx-environment'; -export const OVSXClientProvider = Symbol('OVSXClientProvider'); -export type OVSXClientProvider = () => Promise; +export const OVSXUrlResolver = Symbol('OVSXUrlResolver') as symbol & interfaces.Abstract; +export type OVSXUrlResolver = (value: string) => MaybePromise; +export const OVSXClientProvider = Symbol('OVSXClientProvider') as symbol & interfaces.Abstract; +export type OVSXClientProvider = () => MaybePromise; + +/** + * @deprecated since 1.32.0 + */ export async function createOVSXClient(vsxEnvironment: VSXEnvironment, requestService: RequestService): Promise { - const [apiVersion, apiUrl] = await Promise.all([ - vsxEnvironment.getVscodeApiVersion(), - vsxEnvironment.getRegistryApiUri() - ]); - return new OVSXClient({ apiVersion, apiUrl: apiUrl.toString() }, requestService); + const apiUrl = await vsxEnvironment.getRegistryApiUri(); + return new OVSXHttpClient(apiUrl, requestService); } diff --git a/packages/vsx-registry/src/common/vsx-environment.ts b/packages/vsx-registry/src/common/vsx-environment.ts index d461b0eb827a3..4366d5ef56dc7 100644 --- a/packages/vsx-registry/src/common/vsx-environment.ts +++ b/packages/vsx-registry/src/common/vsx-environment.ts @@ -14,6 +14,8 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import type { OVSXRouterConfig } from '@theia/ovsx-client'; + export const VSX_ENVIRONMENT_PATH = '/services/vsx-environment'; export const VSXEnvironment = Symbol('VSXEnvironment'); @@ -21,4 +23,5 @@ export interface VSXEnvironment { getRegistryUri(): Promise; getRegistryApiUri(): Promise; getVscodeApiVersion(): Promise; + getOvsxRouterConfig?(): Promise; } diff --git a/packages/vsx-registry/src/common/vsx-registry-common-module.ts b/packages/vsx-registry/src/common/vsx-registry-common-module.ts new file mode 100644 index 0000000000000..0fd32471a58f7 --- /dev/null +++ b/packages/vsx-registry/src/common/vsx-registry-common-module.ts @@ -0,0 +1,63 @@ +// ***************************************************************************** +// 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 { ContainerModule } from '@theia/core/shared/inversify'; +import { OVSXClientProvider, OVSXUrlResolver } from '../common'; +import { RequestService } from '@theia/core/shared/@theia/request'; +import { ExtensionIdMatchesFilterFactory, OVSXApiFilter, OVSXApiFilterImpl, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client'; +import { VSXEnvironment } from './vsx-environment'; + +export default new ContainerModule(bind => { + bind(OVSXUrlResolver) + .toFunction(url => url); + bind(OVSXClientProvider) + .toDynamicValue(ctx => { + const vsxEnvironment = ctx.container.get(VSXEnvironment); + const requestService = ctx.container.get(RequestService); + const urlResolver = ctx.container.get(OVSXUrlResolver); + const clientPromise = Promise + .all([ + vsxEnvironment.getRegistryApiUri(), + vsxEnvironment.getOvsxRouterConfig?.(), + ]) + .then(async ([apiUrl, ovsxRouterConfig]) => { + if (ovsxRouterConfig) { + const clientFactory = OVSXHttpClient.createClientFactory(requestService); + return OVSXRouterClient.FromConfig( + ovsxRouterConfig, + async url => clientFactory(await urlResolver(url)), + [RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory] + ); + } + return new OVSXHttpClient( + await urlResolver(apiUrl), + requestService + ); + }); + // reuse the promise for subsequent calls to this provider + return () => clientPromise; + }) + .inSingletonScope(); + bind(OVSXApiFilter) + .toDynamicValue(ctx => { + const vsxEnvironment = ctx.container.get(VSXEnvironment); + const apiFilter = new OVSXApiFilterImpl('-- temporary invalid version value --'); + vsxEnvironment.getVscodeApiVersion() + .then(apiVersion => apiFilter.supportedApiVersion = apiVersion); + return apiFilter; + }) + .inSingletonScope(); +}); diff --git a/packages/vsx-registry/src/node/vsx-cli.ts b/packages/vsx-registry/src/node/vsx-cli.ts new file mode 100644 index 0000000000000..c0ce5a9f06c61 --- /dev/null +++ b/packages/vsx-registry/src/node/vsx-cli.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// 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 { CliContribution } from '@theia/core/lib/node'; +import { injectable } from '@theia/core/shared/inversify'; +import { Argv } from '@theia/core/shared/yargs'; +import { OVSXRouterConfig } from '@theia/ovsx-client'; +import * as fs from 'fs'; + +@injectable() +export class VsxCli implements CliContribution { + + ovsxRouterConfig: OVSXRouterConfig | undefined; + + configure(conf: Argv<{}>): void { + conf.option('ovsx-router-config', { description: 'JSON configuration file for the OVSX router client', type: 'string' }); + } + + async setArguments(args: Record): Promise { + const { 'ovsx-router-config': ovsxRouterConfig } = args; + if (typeof ovsxRouterConfig === 'string') { + this.ovsxRouterConfig = JSON.parse(await fs.promises.readFile(ovsxRouterConfig, 'utf8')); + } + } +} diff --git a/packages/vsx-registry/src/node/vsx-environment-impl.ts b/packages/vsx-registry/src/node/vsx-environment-impl.ts index 850866c585545..ff094b09c41f7 100644 --- a/packages/vsx-registry/src/node/vsx-environment-impl.ts +++ b/packages/vsx-registry/src/node/vsx-environment-impl.ts @@ -14,10 +14,12 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { OVSXRouterConfig } from '@theia/ovsx-client'; import { PluginVsCodeCliContribution } from '@theia/plugin-ext-vscode/lib/node/plugin-vscode-cli-contribution'; import { VSXEnvironment } from '../common/vsx-environment'; +import { VsxCli } from './vsx-cli'; @injectable() export class VSXEnvironmentImpl implements VSXEnvironment { @@ -27,6 +29,9 @@ export class VSXEnvironmentImpl implements VSXEnvironment { @inject(PluginVsCodeCliContribution) protected readonly pluginVscodeCli: PluginVsCodeCliContribution; + @inject(VsxCli) + protected vsxCli: VsxCli; + async getRegistryUri(): Promise { return this._registryUri.toString(true); } @@ -38,4 +43,8 @@ export class VSXEnvironmentImpl implements VSXEnvironment { async getVscodeApiVersion(): Promise { return this.pluginVscodeCli.vsCodeApiVersionPromise; } + + async getOvsxRouterConfig(): Promise { + return this.vsxCli.ovsxRouterConfig; + } } diff --git a/packages/vsx-registry/src/node/vsx-extension-resolver.ts b/packages/vsx-registry/src/node/vsx-extension-resolver.ts index 26e63be0bfb31..2ea154702b4c6 100644 --- a/packages/vsx-registry/src/node/vsx-extension-resolver.ts +++ b/packages/vsx-registry/src/node/vsx-extension-resolver.ts @@ -22,7 +22,7 @@ import URI from '@theia/core/lib/common/uri'; import { PluginDeployerHandler, PluginDeployerResolver, PluginDeployerResolverContext, PluginDeployOptions, PluginIdentifiers } from '@theia/plugin-ext/lib/common/plugin-protocol'; import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri'; import { OVSXClientProvider } from '../common/ovsx-client-provider'; -import { VSXExtensionRaw } from '@theia/ovsx-client'; +import { OVSXApiFilter, VSXExtensionRaw } from '@theia/ovsx-client'; import { RequestService } from '@theia/core/shared/@theia/request'; import { PluginVSCodeEnvironment } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-environment'; import { PluginUninstallationManager } from '@theia/plugin-ext/lib/main/node/plugin-uninstallation-manager'; @@ -35,6 +35,7 @@ export class VSXExtensionResolver implements PluginDeployerResolver { @inject(RequestService) protected requestService: RequestService; @inject(PluginVSCodeEnvironment) protected readonly environment: PluginVSCodeEnvironment; @inject(PluginUninstallationManager) protected readonly uninstallationManager: PluginUninstallationManager; + @inject(OVSXApiFilter) protected vsxApiFilter: OVSXApiFilter; accept(pluginId: string): boolean { return !!VSCodeExtensionUri.toId(new URI(pluginId)); @@ -49,10 +50,12 @@ export class VSXExtensionResolver implements PluginDeployerResolver { const client = await this.clientProvider(); if (options) { console.log(`[${id}]: trying to resolve version ${options.version}...`); - extension = await client.getExtension(id, { extensionVersion: options.version, includeAllVersions: true }); + const { extensions } = await client.query({ extensionId: id, extensionVersion: options.version, includeAllVersions: true }); + extension = extensions[0]; } else { console.log(`[${id}]: trying to resolve latest version...`); - extension = await client.getLatestCompatibleExtensionVersion(id); + const { extensions } = await client.query({ extensionId: id }); + extension = this.vsxApiFilter.getLatestCompatibleExtension(extensions); } if (!extension) { return; diff --git a/packages/vsx-registry/src/node/vsx-registry-backend-module.ts b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts index 5491c8025ff4f..b060f4bc91a84 100644 --- a/packages/vsx-registry/src/node/vsx-registry-backend-module.ts +++ b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts @@ -14,24 +14,22 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** +import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; +import { CliContribution } from '@theia/core/lib/node'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { VSXExtensionResolver } from './vsx-extension-resolver'; import { PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol'; -import { OVSXClientProvider, createOVSXClient } from '../common/ovsx-client-provider'; import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment'; +import { VsxCli } from './vsx-cli'; import { VSXEnvironmentImpl } from './vsx-environment-impl'; -import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; -import { RequestService } from '@theia/core/shared/@theia/request'; +import { VSXExtensionResolver } from './vsx-extension-resolver'; export default new ContainerModule(bind => { - bind(OVSXClientProvider).toDynamicValue(ctx => { - const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment), ctx.container.get(RequestService)); - return () => clientPromise; - }).inSingletonScope(); bind(VSXEnvironment).to(VSXEnvironmentImpl).inSingletonScope(); - bind(ConnectionHandler).toDynamicValue( - ctx => new JsonRpcConnectionHandler(VSX_ENVIRONMENT_PATH, () => ctx.container.get(VSXEnvironment)) - ).inSingletonScope(); + bind(VsxCli).toSelf().inSingletonScope(); + bind(CliContribution).toService(VsxCli); + bind(ConnectionHandler) + .toDynamicValue(ctx => new JsonRpcConnectionHandler(VSX_ENVIRONMENT_PATH, () => ctx.container.get(VSXEnvironment))) + .inSingletonScope(); bind(VSXExtensionResolver).toSelf().inSingletonScope(); bind(PluginDeployerResolver).toService(VSXExtensionResolver); }); diff --git a/sample-plugins/sample-namespace/plugin-a/LICENSE b/sample-plugins/sample-namespace/plugin-a/LICENSE new file mode 100644 index 0000000000000..e48e0963459bf --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-a/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"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: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/sample-plugins/sample-namespace/plugin-a/README.md b/sample-plugins/sample-namespace/plugin-a/README.md new file mode 100644 index 0000000000000..99f8d56d594e0 --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-a/README.md @@ -0,0 +1,3 @@ +# plugin-a + +This is plugin-a's README diff --git a/sample-plugins/sample-namespace/plugin-a/extension.js b/sample-plugins/sample-namespace/plugin-a/extension.js new file mode 100644 index 0000000000000..cc954154f7893 --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-a/extension.js @@ -0,0 +1,23 @@ +// ***************************************************************************** +// 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 WITH Classpath-exception-2.0 +// ***************************************************************************** + +const vscode = require('vscode'); + +exports.activate = function (context) { + context.subscriptions.push(vscode.commands.registerCommand('plugin-a.hello', () => { + vscode.window.showInformationMessage('Hello from plugin-a!'); + })); +} diff --git a/sample-plugins/sample-namespace/plugin-a/icon128.png b/sample-plugins/sample-namespace/plugin-a/icon128.png new file mode 100644 index 0000000000000..090de9e968d0d Binary files /dev/null and b/sample-plugins/sample-namespace/plugin-a/icon128.png differ diff --git a/sample-plugins/sample-namespace/plugin-a/package.json b/sample-plugins/sample-namespace/plugin-a/package.json new file mode 100644 index 0000000000000..4f957e706740e --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-a/package.json @@ -0,0 +1,31 @@ +{ + "name": "plugin-a", + "version": "1.0.0", + "main": "extension.js", + "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" + }, + "engines": { + "vscode": "^1.50.0" + }, + "activationEvents": [ + "onCommand:plugin-a.hello" + ], + "devDependencies": { + "@types/vscode": "^1.50.0" + }, + "scripts": { + "prepare": "yarn -s package", + "package": "vsce package --yarn" + }, + "contributes": { + "commands": [ + { + "command": "plugin-a.hello", + "title": "Hello from plugin-a" + } + ] + } +} diff --git a/sample-plugins/sample-namespace/plugin-b/LICENSE b/sample-plugins/sample-namespace/plugin-b/LICENSE new file mode 100644 index 0000000000000..e48e0963459bf --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-b/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"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: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/sample-plugins/sample-namespace/plugin-b/README.md b/sample-plugins/sample-namespace/plugin-b/README.md new file mode 100644 index 0000000000000..a6f20f21eb796 --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-b/README.md @@ -0,0 +1,3 @@ +# plugin-b + +This is plugin-b's README. diff --git a/sample-plugins/sample-namespace/plugin-b/extension.js b/sample-plugins/sample-namespace/plugin-b/extension.js new file mode 100644 index 0000000000000..a4e01b37c1ff0 --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-b/extension.js @@ -0,0 +1,23 @@ +// ***************************************************************************** +// 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 WITH Classpath-exception-2.0 +// ***************************************************************************** + +const vscode = require('vscode'); + +exports.activate = function (context) { + context.subscriptions.push(vscode.commands.registerCommand('plugin-b.hello', () => { + vscode.window.showInformationMessage('Hello from plugin-b!'); + })); +} diff --git a/sample-plugins/sample-namespace/plugin-b/icon128.png b/sample-plugins/sample-namespace/plugin-b/icon128.png new file mode 100644 index 0000000000000..090de9e968d0d Binary files /dev/null and b/sample-plugins/sample-namespace/plugin-b/icon128.png differ diff --git a/sample-plugins/sample-namespace/plugin-b/package.json b/sample-plugins/sample-namespace/plugin-b/package.json new file mode 100644 index 0000000000000..1d2e86f009a7f --- /dev/null +++ b/sample-plugins/sample-namespace/plugin-b/package.json @@ -0,0 +1,31 @@ +{ + "name": "plugin-b", + "version": "1.0.0", + "main": "extension.js", + "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" + }, + "engines": { + "vscode": "^1.50.0" + }, + "activationEvents": [ + "onCommand:plugin-b.hello" + ], + "devDependencies": { + "@types/vscode": "^1.50.0" + }, + "scripts": { + "prepare": "yarn -s package", + "package": "vsce package --yarn" + }, + "contributes": { + "commands": [ + { + "command": "plugin-b.hello", + "title": "Hello from plugin-b" + } + ] + } +} diff --git a/yarn.lock b/yarn.lock index 9a70990877ef3..b511dbdf93121 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2199,6 +2199,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.5.tgz#b1d2f772142a301538fae9bdf9cf15b9f2573a29" integrity sha512-hKB88y3YHL8oPOs/CNlaXtjWn93+Bs48sDQR37ZUqG2tLeCS7EA1cmnkKsuQsub9OKEB/y/Rw9zqJqqNSbqVlQ== +"@types/vscode@^1.50.0": + version "1.79.1" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.79.1.tgz#ab568315f9c844a8f4fa8f168b2d6dbf1f58dd02" + integrity sha512-Ikwc4YbHABzqthrWfeAvItaAIfX9mdjMWxqNgTpGjhgOu0TMRq9LzyZ2yBK0JhYqoSjEubEPawf6zJgnl6Egtw== + "@types/which@^1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf" @@ -2400,6 +2405,33 @@ https-proxy-agent "^5.0.0" proxy-from-env "^1.1.0" +"@vscode/vsce@^2.15.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@vscode/vsce/-/vsce-2.16.0.tgz#a3ddcf7e84914576f35d891e236bc496c568776f" + integrity sha512-BhJ0zO7UxShLFBZM6jwOLt1ZVoqQ4r5Lj/kHNeYp0ICPXhz/erqBSMQnHkRgkjn2L/bh+TYFGkZyguhu/SKsjw== + dependencies: + azure-devops-node-api "^11.0.1" + chalk "^2.4.2" + cheerio "^1.0.0-rc.9" + commander "^6.1.0" + glob "^7.0.6" + hosted-git-info "^4.0.2" + leven "^3.1.0" + markdown-it "^12.3.2" + mime "^1.3.4" + minimatch "^3.0.3" + parse-semver "^1.1.1" + read "^1.0.7" + semver "^5.1.0" + tmp "^0.2.1" + typed-rest-client "^1.8.4" + url-join "^4.0.1" + xml2js "^0.4.23" + yauzl "^2.3.1" + yazl "^2.2.2" + optionalDependencies: + keytar "^7.7.0" + "@vscode/windows-ca-certs@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.1.tgz#35c88b2d2a52f7759bfb6878906c3d40421ec6a3" @@ -3015,6 +3047,14 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +azure-devops-node-api@^11.0.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz#bf04edbef60313117a0507415eed4790a420ad6b" + integrity sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA== + dependencies: + tunnel "0.0.6" + typed-rest-client "^1.8.4" + babel-loader@^8.2.2: version "8.3.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" @@ -3189,6 +3229,11 @@ body-parser@1.20.1, body-parser@^1.17.2, body-parser@^1.18.3: type-is "~1.6.18" unpipe "1.0.0" +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + boolean@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" @@ -3486,7 +3531,7 @@ chalk@4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -3513,6 +3558,31 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.9: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + chokidar@3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -3706,6 +3776,11 @@ commander@^2.12.1, commander@^2.20.0, commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + commander@^7.0.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" @@ -4037,6 +4112,22 @@ css-loader@^6.2.0: postcss-value-parser "^4.2.0" semver "^7.3.8" +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4319,7 +4410,7 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== -detect-libc@^2.0.1: +detect-libc@^2.0.0, detect-libc@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== @@ -4390,6 +4481,20 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -4397,11 +4502,27 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" +domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + dompurify@^2.2.9: version "2.4.4" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.4.tgz#c17803931dd524e1b68e0e940a84567f9498f4bd" integrity sha512-1e2SpqHiRx4DPvmRuXU5J0di3iQACwJM+mFGE2HAkkK7Tbnfk9WcghcAmyWc9CRrjyRRUpmuhPUH6LphQQR3EQ== +domutils@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" + integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.1" + dot-prop@6.0.1, dot-prop@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" @@ -4610,10 +4731,10 @@ enquirer@^2.3.5, enquirer@~2.3.6: dependencies: ansi-colors "^4.1.1" -entities@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" + integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== entities@~2.1.0: version "2.1.0" @@ -5698,7 +5819,7 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: +glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5918,7 +6039,7 @@ hosted-git-info@^3.0.6: dependencies: lru-cache "^6.0.0" -hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1, hosted-git-info@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== @@ -5944,6 +6065,16 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +htmlparser2@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010" + integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + domutils "^3.0.1" + entities "^4.3.0" + http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" @@ -6774,6 +6905,14 @@ keytar@7.2.0: node-addon-api "^3.0.0" prebuild-install "^6.0.0" +keytar@^7.7.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb" + integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ== + dependencies: + node-addon-api "^4.3.0" + prebuild-install "^7.0.1" + keyv@^4.0.0: version "4.5.2" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.2.tgz#0e310ce73bf7851ec702f2eaf46ec4e3805cce56" @@ -6884,6 +7023,11 @@ less@^3.0.3: native-request "^1.0.5" source-map "~0.6.0" +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -7322,7 +7466,7 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.24, dependencies: mime-db "1.52.0" -mime@1.6.0, mime@^1.4.1: +mime@1.6.0, mime@^1.3.4, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -7390,7 +7534,7 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.0, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -7750,11 +7894,23 @@ node-abi@^2.21.0, node-abi@^2.7.0: dependencies: semver "^5.4.1" +node-abi@^3.3.0: + version "3.30.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.30.0.tgz#d84687ad5d24ca81cdfa912a36f2c5c19b137359" + integrity sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw== + dependencies: + semver "^7.3.5" + node-addon-api@^3.0.0, node-addon-api@^3.0.2, node-addon-api@^3.1.0, node-addon-api@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-addon-api@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + node-addon-api@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" @@ -8069,6 +8225,13 @@ nsfw@^2.2.4: dependencies: node-addon-api "^5.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -8549,6 +8712,13 @@ parse-path@^7.0.0: dependencies: protocols "^2.0.0" +parse-semver@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/parse-semver/-/parse-semver-1.1.1.tgz#9a4afd6df063dc4826f93fba4a99cf223f666cb8" + integrity sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ== + dependencies: + semver "^5.1.0" + parse-url@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-8.1.0.tgz#972e0827ed4b57fc85f0ea6b0d839f0d8a57a57d" @@ -8556,6 +8726,14 @@ parse-url@^8.1.0: dependencies: parse-path "^7.0.0" +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" @@ -8817,6 +8995,24 @@ prebuild-install@^6.0.0: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +prebuild-install@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -9014,7 +9210,7 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qs@6.11.0, qs@^6.9.4: +qs@6.11.0, qs@^6.9.1, qs@^6.9.4: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -9533,6 +9729,11 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + saxes@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" @@ -9599,7 +9800,7 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -9777,6 +9978,15 @@ simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + sinon@^12.0.0: version "12.0.1" resolved "https://registry.yarnpkg.com/sinon/-/sinon-12.0.1.tgz#331eef87298752e1b88a662b699f98e403c859e9" @@ -10404,7 +10614,7 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" -tmp@~0.2.1: +tmp@^0.2.1, tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== @@ -10570,6 +10780,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -10651,6 +10866,15 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +typed-rest-client@^1.8.4: + version "1.8.9" + resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.9.tgz#e560226bcadfe71b0fb5c416b587f8da3b8f92d8" + integrity sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g== + dependencies: + qs "^6.9.1" + tunnel "0.0.6" + underscore "^1.12.1" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -10726,6 +10950,11 @@ unbzip2-stream@1.4.3, unbzip2-stream@^1.0.9: buffer "^5.2.1" through "^2.3.8" +underscore@^1.12.1: + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -10828,6 +11057,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + url-parse@^1.5.3: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" @@ -11300,6 +11534,19 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" @@ -11436,7 +11683,7 @@ yargs@^17.0.1, yargs@^17.6.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yauzl@^2.10.0, yauzl@^2.4.2: +yauzl@^2.10.0, yauzl@^2.3.1, yauzl@^2.4.2: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== @@ -11444,6 +11691,13 @@ yauzl@^2.10.0, yauzl@^2.4.2: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" +yazl@^2.2.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== + dependencies: + buffer-crc32 "~0.2.3" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"