diff --git a/.travis.yml b/.travis.yml index 152e90e0b0774..c3d875e67f832 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,6 +55,7 @@ cache: - packages/typehierarchy/node_modules - packages/userstorage/node_modules - packages/variable-resolver/node_modules + - packages/vsx-registry/node_modules - packages/workspace/node_modules # end_cache_directories before_cache: diff --git a/configs/root-compilation.tsconfig.json b/configs/root-compilation.tsconfig.json index 3154544acd417..84e995ad79684 100644 --- a/configs/root-compilation.tsconfig.json +++ b/configs/root-compilation.tsconfig.json @@ -135,6 +135,9 @@ }, { "path": "../examples/electron/compile.tsconfig.json" + }, + { + "path": "../packages/vsx-registry/compile.tsconfig.json" } ] } diff --git a/examples/browser/package.json b/examples/browser/package.json index bb8becec87883..14826c5efefe6 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -51,6 +51,7 @@ "@theia/typehierarchy": "^0.16.0", "@theia/userstorage": "^0.16.0", "@theia/variable-resolver": "^0.16.0", + "@theia/vsx-registry": "^0.16.0", "@theia/workspace": "^0.16.0" }, "scripts": { diff --git a/examples/electron/compile.tsconfig.json b/examples/electron/compile.tsconfig.json index 3dff9bf742620..9d49027cf3efc 100644 --- a/examples/electron/compile.tsconfig.json +++ b/examples/electron/compile.tsconfig.json @@ -121,6 +121,9 @@ }, { "path": "../../packages/workspace/compile.tsconfig.json" + }, + { + "path": "../../packages/vsx-registry/compile.tsconfig.json" } ] } diff --git a/examples/electron/package.json b/examples/electron/package.json index 3ca6031654259..ac4f76ee82c6b 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -50,6 +50,7 @@ "@theia/typehierarchy": "^0.16.0", "@theia/userstorage": "^0.16.0", "@theia/variable-resolver": "^0.16.0", + "@theia/vsx-registry": "^0.16.0", "@theia/workspace": "^0.16.0" }, "scripts": { diff --git a/packages/vsx-registry/.eslintrc.js b/packages/vsx-registry/.eslintrc.js new file mode 100644 index 0000000000000..be9cf1a1b3dff --- /dev/null +++ b/packages/vsx-registry/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'compile.tsconfig.json' + } +}; diff --git a/packages/vsx-registry/README.md b/packages/vsx-registry/README.md new file mode 100644 index 0000000000000..244cede95ab87 --- /dev/null +++ b/packages/vsx-registry/README.md @@ -0,0 +1,36 @@ +
+ +
+ +theia-ext-logo + +

THEIA - Open VSX Registry Extension

+ +
+ +
+ +## Description + +The `@theia/vsx-registry` extension provides integration with the Open VSX Registry. + +### Configuration + +The extension connects to the plubic Open VSX Registry hosted on `http://open-vsx.org/`. +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. + +## Additional Information + +- [API documentation for `@theia/vsx-registry`](https://eclipse-theia.github.io/theia/docs/next/modules/vsx-registry.html) +- [Theia - GitHub](https://github.com/eclipse-theia/theia) +- [Theia - Website](https://theia-ide.org/) + +## License + +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) + +## Trademark +"Theia" is a trademark of the Eclipse Foundation +https://www.eclipse.org/theia diff --git a/packages/vsx-registry/compile.tsconfig.json b/packages/vsx-registry/compile.tsconfig.json new file mode 100644 index 0000000000000..dba687d73e555 --- /dev/null +++ b/packages/vsx-registry/compile.tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../core/compile.tsconfig.json" + }, + { + "path": "../plugin-ext-vscode/compile.tsconfig.json" + } + ] +} diff --git a/packages/vsx-registry/package.json b/packages/vsx-registry/package.json new file mode 100644 index 0000000000000..92d72eb8b1a1b --- /dev/null +++ b/packages/vsx-registry/package.json @@ -0,0 +1,57 @@ +{ + "name": "@theia/vsx-registry", + "version": "0.16.0", + "description": "Theia - VSX Registry", + "dependencies": { + "@theia/core": "^0.16.0", + "@theia/plugin-ext-vscode": "^0.16.0", + "@types/bent": "^7.0.1", + "@types/sanitize-html": "^1.13.31", + "@types/showdown": "^1.7.1", + "bent": "^7.1.0", + "fs-extra": "^4.0.2", + "p-debounce": "^2.1.0", + "requestretry": "^3.1.0", + "sanitize-html": "^1.14.1", + "showdown": "^1.9.1", + "uuid": "^3.2.1" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "theia-extension" + ], + "theiaExtensions": [ + { + "frontend": "lib/browser/vsx-registry-frontend-module", + "backend": "lib/node/vsx-registry-backend-module" + } + ], + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "lint": "theiaext lint", + "build": "theiaext build", + "watch": "theiaext watch", + "clean": "theiaext clean", + "test": "theiaext test" + }, + "devDependencies": { + "@theia/ext-scripts": "^0.16.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/vsx-registry/src/browser/style/defaultIcon.png b/packages/vsx-registry/src/browser/style/defaultIcon.png new file mode 100644 index 0000000000000..adad56268218c Binary files /dev/null and b/packages/vsx-registry/src/browser/style/defaultIcon.png differ diff --git a/packages/vsx-registry/src/browser/style/extensions.svg b/packages/vsx-registry/src/browser/style/extensions.svg new file mode 100644 index 0000000000000..b7cd62ad53907 --- /dev/null +++ b/packages/vsx-registry/src/browser/style/extensions.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/vsx-registry/src/browser/style/index.css b/packages/vsx-registry/src/browser/style/index.css new file mode 100644 index 0000000000000..7240df47ea591 --- /dev/null +++ b/packages/vsx-registry/src/browser/style/index.css @@ -0,0 +1,286 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +:root { + --theia-vsx-extension-icon-size: calc(var(--theia-ui-icon-font-size)*3); + --theia-vsx-extension-editor-icon-size: calc(var(--theia-vsx-extension-icon-size)*3); +} + +.theia-vsx-extensions-icon { + -webkit-mask: url('extensions.svg'); + mask: url('extensions.svg'); +} + +.theia-vsx-extensions { + height: 100%; +} + +.theia-vsx-extension, +.theia-vsx-extensions-view-container .part > .body { + min-height: calc(var(--theia-content-line-height)*3) +} + +.theia-vsx-extensions-search-bar { + padding: var(--theia-ui-padding); + display: flex; + align-content: center; +} + +.theia-vsx-extensions-search-bar .theia-input { + line-height: var(--theia-content-line-height); + flex: 1; + padding-top: calc(var(--theia-ui-padding)/2); + padding-bottom: calc(var(--theia-ui-padding)/2); +} + +.theia-vsx-extension { + display: flex; + flex-direction: row; +} + +.theia-vsx-extension-icon { + height: var(--theia-vsx-extension-icon-size); + width: var(--theia-vsx-extension-icon-size); + align-self: center; + padding-right: calc(var(--theia-ui-padding)*2.5); + flex-shrink: 0; + object-fit: contain; +} + +.theia-vsx-extension-icon.placeholder { + background-size: var(--theia-vsx-extension-icon-size); + background-repeat: no-repeat; + background-image: url('defaultIcon.png'); +} + +.theia-vsx-extension-content { + display: flex; + flex-direction: column; + width: calc(100% - var(--theia-vsx-extension-icon-size) - var(--theia-ui-padding)*2.5); +} + +.theia-vsx-extension-content .title { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + white-space: nowrap; +} + +.theia-vsx-extension-content .title .name { + font-weight: 700; +} + +.theia-vsx-extension-content .title .version, +.theia-vsx-extension-content .title .stat { + opacity: .85; + font-size: 80%; +} + + +.theia-vsx-extension-content .title .stat .average-rating > i { + color: #ff8e00; +} + +.theia-vsx-extension-content .title .stat .average-rating > i, +.theia-vsx-extension-content .title .stat .download-count > i { + padding-right: calc(var(--theia-ui-padding)/2); +} + +.theia-vsx-extension-content .title .stat .average-rating, +.theia-vsx-extension-content .title .stat .download-count { + padding-left: var(--theia-ui-padding); +} + +.theia-vsx-extension-description { + padding-right: calc(var(--theia-ui-padding)*2); +} + +.theia-vsx-extension-publisher { + font-weight: 600; + font-size: 90%; +} + +.theia-vsx-extension-action-bar { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.theia-vsx-extension-action-bar .action { + font-size: 80%; + min-width: auto !important; + padding: 2px var(--theia-ui-padding) !important; + margin-top: 2px; +} + +/* Editor Section */ + +.theia-vsx-extension-editor { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + padding: var(--theia-ui-padding); +} + +.theia-vsx-extension-editor .header { + display: flex; + padding: calc(var(--theia-ui-padding)*3) calc(var(--theia-ui-padding)*3) calc(var(--theia-ui-padding)*2); + overflow: hidden; + flex-shrink: 0; +} + +.theia-vsx-extension-editor .body { + flex: 1; +} + +.theia-vsx-extension-editor .header .icon-container { + height: var(--theia-vsx-extension-editor-icon-size); + width: var(--theia-vsx-extension-editor-icon-size); + align-self: center; + padding-right: calc(var(--theia-ui-padding)*2.5); + flex-shrink: 0; + object-fit: contain; +} + +.theia-vsx-extension-editor .header .icon-container.placeholder { + background-size: var(--theia-vsx-extension-editor-icon-size); + background-repeat: no-repeat; + background-image: url('defaultIcon.png'); +} + +.theia-vsx-extension-editor .header .details { + overflow: hidden; + user-select: text; + -webkit-user-select: text; +} + +.theia-vsx-extension-editor .header .details .title, +.theia-vsx-extension-editor .header .details .subtitle { + display: flex; + align-items: center; +} + +.theia-vsx-extension-editor .header .details .title .name { + flex: 0; + font-size: calc(var(--theia-ui-font-size1)*2); + font-weight: 600; + white-space: nowrap; + cursor: pointer; +} + +.theia-vsx-extension-editor .header .details .title .identifier { + margin-left: calc(var(--theia-ui-padding)*5/3); + opacity: .6; + background: hsla(0,0%,68%,.31); + user-select: text; + -webkit-user-select: text; + white-space: nowrap; +} + +.theia-vsx-extension-editor .header .details .title .preview { + background: #d63f26; +} + +.vs .theia-vsx-extension-editor .header .details .title .preview { + color: white; +} + +.theia-vsx-extension-editor .header .details .title .identifier, +.theia-vsx-extension-editor .header .details .title .preview, +.theia-vsx-extension-editor .header .details .title .builtin { + line-height: var(--theia-code-line-height); +} + +.theia-vsx-extension-editor .header .details .title .identifier, +.theia-vsx-extension-editor .header .details .title .preview { + padding: calc(var(--theia-ui-padding)*2/3); + padding-top: 0px; + padding-bottom: 0px; + border-radius: calc(var(--theia-ui-padding)*2/3); +} + + +.theia-vsx-extension-editor .header .details .title .preview, +.theia-vsx-extension-editor .header .details .title .builtin { + font-size: var(--theia-ui-font-size0); + font-style: italic; + margin-left: calc(var(--theia-ui-padding)*5/3); +} + +.theia-vsx-extension-editor .header .details .subtitle { + padding-top: var(--theia-ui-padding); + white-space: nowrap; +} + +.theia-vsx-extension-editor .header .details .subtitle > span { + display: flex; + align-items: center; + cursor: pointer; + padding-right: var(--theia-ui-padding); + line-height: var(--theia-content-line-height); + height: var(--theia-content-line-height); +} + +.theia-vsx-extension-editor .header .details .subtitle > span:not(:first-child):not(:empty) { + border-left: 1px solid hsla(0,0%,50%,.7); + padding-left: var(--theia-ui-padding); +} + +.theia-vsx-extension-editor .header .details .subtitle .publisher { + font-size: var(--theia-ui-font-size3); +} + +.theia-vsx-extension-editor .header .details .subtitle .publisher .namespace-access, +.theia-vsx-extension-editor .header .details .subtitle .download-count::before { + padding-right: var(--theia-ui-padding); +} + +.theia-vsx-extension-editor .header .details .subtitle .average-rating > i { + color: #ff8e00; +} + +.theia-vsx-extension-editor .header .details .subtitle .average-rating > i:not(:first-child) { + padding-left: calc(var(--theia-ui-padding)/2); +} + + +.theia-vsx-extension-editor .header .details .description { + margin-top: calc(var(--theia-ui-padding)*5/3); +} + +.theia-vsx-extension-editor .action { + font-weight: 600; + margin-top: calc(var(--theia-ui-padding)*5/3); + margin-left: 0px; + padding: 1px var(--theia-ui-padding); +} + +/** Theming */ + +.theia-vsx-extension-editor .action.prominent, +.theia-vsx-extension-action-bar .action.prominent { + color: var(--theia-extensionButton-prominentForeground); + background-color: var(--theia-extensionButton-prominentBackground); +} + +.theia-vsx-extension-editor .action.prominent:hover, +.theia-vsx-extension-action-bar .action.prominent:hover { + background-color: var(--theia-extensionButton-prominentHoverBackground); +} + diff --git a/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts b/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts new file mode 100644 index 0000000000000..f7febf5c704d3 --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { WidgetOpenHandler } from '@theia/core/lib/browser'; +import { VSXExtensionOptions } from './vsx-extension'; +import { VSXExtensionUri } from '../common/vsx-extension-uri'; +import { VSXExtensionEditor } from './vsx-extension-editor'; + +@injectable() +export class VSXExtensionEditorManager extends WidgetOpenHandler { + + readonly id = VSXExtensionEditor.ID; + + canHandle(uri: URI): number { + const id = VSXExtensionUri.toId(uri); + return !!id ? 500 : 0; + } + + protected createWidgetOptions(uri: URI): VSXExtensionOptions { + const id = VSXExtensionUri.toId(uri); + if (!id) { + throw new Error('Invalid URI: ' + uri.toString()); + } + return { id }; + } + +} diff --git a/packages/vsx-registry/src/browser/vsx-extension-editor.tsx b/packages/vsx-registry/src/browser/vsx-extension-editor.tsx new file mode 100644 index 0000000000000..d242e7c5a53aa --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extension-editor.tsx @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { inject, injectable, postConstruct } from 'inversify'; +import { ReactWidget, Message } from '@theia/core/lib/browser'; +import { VSXExtension } from './vsx-extension'; +import { VSXExtensionsModel } from './vsx-extensions-model'; + +@injectable() +export class VSXExtensionEditor extends ReactWidget { + + static ID = 'vsx-extension-editor'; + + @inject(VSXExtension) + protected readonly extension: VSXExtension; + + @inject(VSXExtensionsModel) + protected readonly model: VSXExtensionsModel; + + @postConstruct() + protected init(): void { + this.addClass('theia-vsx-extension-editor'); + this.id = VSXExtensionEditor.ID + ':' + this.extension.id; + this.title.closable = true; + this.updateTitle(); + this.title.iconClass = 'fa fa-puzzle-piece'; + this.node.tabIndex = -1; + + this.update(); + this.toDispose.push(this.model.onDidChange(() => this.update())); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.node.focus(); + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.updateTitle(); + } + + protected updateTitle(): void { + const label = 'Extension: ' + (this.extension.displayName || this.extension.name); + this.title.label = label; + this.title.caption = label; + } + + protected render(): React.ReactNode { + return this.extension.renderEditor(); + } + +} diff --git a/packages/vsx-registry/src/browser/vsx-extension.tsx b/packages/vsx-registry/src/browser/vsx-extension.tsx new file mode 100644 index 0000000000000..073c4de96348d --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extension.tsx @@ -0,0 +1,503 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { injectable, inject } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { TreeElement } from '@theia/core/lib/browser/source-tree'; +import { OpenerService, open, OpenerOptions } from '@theia/core/lib/browser/opener-service'; +import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { PluginServer, DeployedPlugin, PluginType } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { VSXExtensionUri } from '../common/vsx-extension-uri'; +import { ProgressService } from '@theia/core/lib/common/progress-service'; +import { Endpoint } from '@theia/core/lib/browser/endpoint'; +import { VSXEnvironment } from '../common/vsx-environment'; +import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; +import { VSXExtensionNamespaceAccess, VSXUser } from '../common/vsx-registry-types'; + +@injectable() +export class VSXExtensionData { + readonly version?: string; + readonly iconUrl?: string; + readonly publisher?: string; + readonly name?: string; + readonly displayName?: string; + readonly description?: string; + readonly averageRating?: number; + readonly downloadCount?: number; + readonly readmeUrl?: string; + readonly licenseUrl?: string; + readonly repository?: string; + readonly license?: string; + readonly readme?: string; + readonly preview?: boolean; + readonly namespaceAccess?: VSXExtensionNamespaceAccess; + readonly publishedBy?: VSXUser; + static KEYS: Set<(keyof VSXExtensionData)> = new Set([ + 'version', + 'iconUrl', + 'publisher', + 'name', + 'displayName', + 'description', + 'averageRating', + 'downloadCount', + 'readmeUrl', + 'licenseUrl', + 'repository', + 'license', + 'readme', + 'preview', + 'namespaceAccess', + 'publishedBy' + ]); +} + +@injectable() +export class VSXExtensionOptions { + readonly id: string; +} + +export const VSXExtensionFactory = Symbol('VSXExtensionFactory'); +export type VSXExtensionFactory = (options: VSXExtensionOptions) => VSXExtension; + +@injectable() +export class VSXExtension implements VSXExtensionData, TreeElement { + + @inject(VSXExtensionOptions) + protected readonly options: VSXExtensionOptions; + + @inject(OpenerService) + protected readonly openerService: OpenerService; + + @inject(HostedPluginSupport) + protected readonly pluginSupport: HostedPluginSupport; + + @inject(PluginServer) + protected readonly pluginServer: PluginServer; + + @inject(ProgressService) + protected readonly progressService: ProgressService; + + @inject(VSXEnvironment) + readonly environment: VSXEnvironment; + + @inject(VSXExtensionsSearchModel) + readonly search: VSXExtensionsSearchModel; + + protected readonly data: Partial = {}; + + get uri(): URI { + return VSXExtensionUri.toUri(this.id); + } + + get id(): string { + return this.options.id; + } + + get visible(): boolean { + return !!this.name; + } + + get plugin(): DeployedPlugin | undefined { + return this.pluginSupport.getPlugin(this.id); + } + + get installed(): boolean { + return !!this.plugin; + } + + get builtin(): boolean { + const plugin = this.plugin; + const type = plugin && plugin.type; + return type === PluginType.System; + } + + update(data: Partial): void { + for (const key of VSXExtensionData.KEYS) { + if (key in data) { + Object.assign(this.data, { [key]: data[key] }); + } + } + } + + protected getData(key: K): VSXExtensionData[K] { + const plugin = this.plugin; + const model = plugin && plugin.metadata.model; + if (model && key in model) { + return model[key as keyof typeof model] as VSXExtensionData[K]; + } + return this.data[key]; + } + + get iconUrl(): string | undefined { + const plugin = this.plugin; + const iconUrl = plugin && plugin.metadata.model.iconUrl; + if (iconUrl) { + return new Endpoint({ path: iconUrl }).getRestUrl().toString(); + } + return this.data['iconUrl']; + } + + get publisher(): string | undefined { + return this.getData('publisher'); + } + + get name(): string | undefined { + return this.getData('name'); + } + + get displayName(): string | undefined { + return this.getData('displayName') || this.name; + } + + get description(): string | undefined { + return this.getData('description'); + } + + get version(): string | undefined { + return this.getData('version'); + } + + get averageRating(): number | undefined { + return this.getData('averageRating'); + } + + get downloadCount(): number | undefined { + return this.getData('downloadCount'); + } + + get readmeUrl(): string | undefined { + const plugin = this.plugin; + const readmeUrl = plugin && plugin.metadata.model.readmeUrl; + if (readmeUrl) { + return new Endpoint({ path: readmeUrl }).getRestUrl().toString(); + } + return this.data['readmeUrl']; + } + + get licenseUrl(): string | undefined { + const plugin = this.plugin; + const licenseUrl = plugin && plugin.metadata.model.licenseUrl; + if (licenseUrl) { + return new Endpoint({ path: licenseUrl }).getRestUrl().toString(); + } + return this.data['licenseUrl']; + } + + get repository(): string | undefined { + return this.getData('repository'); + } + + get license(): string | undefined { + return this.getData('license'); + } + + get readme(): string | undefined { + return this.getData('readme'); + } + + get preview(): boolean | undefined { + return this.getData('preview'); + } + + get namespaceAccess(): VSXExtensionNamespaceAccess | undefined { + return this.getData('namespaceAccess'); + } + + get publishedBy(): VSXUser | undefined { + return this.getData('publishedBy'); + } + + protected _busy = 0; + get busy(): boolean { + return !!this._busy; + } + + async install(): Promise { + this._busy++; + try { + await this.progressService.withProgress(`"Installing '${this.id}' extension...`, 'extensions', () => + this.pluginServer.deploy(this.uri.toString()) + ); + } finally { + this._busy--; + } + } + + async uninstall(): Promise { + this._busy++; + try { + await this.progressService.withProgress(`Uninstalling '${this.id}' extension...`, 'extensions', () => + this.pluginServer.undeploy(this.id) + ); + } finally { + this._busy--; + } + } + + async open(options: OpenerOptions = { mode: 'reveal' }): Promise { + await this.doOpen(this.uri, options); + } + + async doOpen(uri: URI, options?: OpenerOptions): Promise { + await open(this.openerService, uri, options); + } + + render(): React.ReactNode { + return ; + } + + renderEditor(): React.ReactNode { + return ; + } + +} + +export abstract class AbstractVSXExtensionComponent extends React.Component { + + readonly install = async () => { + this.forceUpdate(); + try { + const pending = this.props.extension.install(); + this.forceUpdate(); + await pending; + } finally { + this.forceUpdate(); + } + }; + + readonly uninstall = async () => { + try { + const pending = this.props.extension.uninstall(); + this.forceUpdate(); + await pending; + } finally { + this.forceUpdate(); + } + }; + + protected renderAction(): React.ReactNode { + const extension = this.props.extension; + const { builtin, busy, installed } = extension; + if (builtin) { + return undefined; + } + if (busy) { + if (installed) { + return ; + } + return ; + } + if (installed) { + return ; + } + return ; + } + +} +export namespace AbstractVSXExtensionComponent { + export interface Props { + extension: VSXExtension + } +} + +const downloadFormatter = new Intl.NumberFormat(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const downloadCompactFormatter = new Intl.NumberFormat(undefined, { notation: 'compact', compactDisplay: 'short' } as any); + +export class VSXExtensionComponent extends AbstractVSXExtensionComponent { + render(): React.ReactNode { + const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating } = this.props.extension; + return
+ {iconUrl ? + : +
} +
+
+
+ {displayName} {version} +
+
+ {downloadCount && {downloadCompactFormatter.format(downloadCount)}} + {averageRating && {averageRating.toFixed(1)}} +
+
+
{description}
+
+ {publisher} + {this.renderAction()} +
+
+
; + } +} + +export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent { + render(): React.ReactNode { + const { + builtin, preview, id, iconUrl, publisher, displayName, description, + averageRating, downloadCount, repository, license, readme + } = this.props.extension; + return +
+ {iconUrl ? + : +
} +
+
+ {displayName} + {id} + {preview && Preview} + {builtin && Built-in} +
+
+ + {this.renderNamespaceAccess()} + {publisher} + + {downloadCount && {downloadFormatter.format(downloadCount)}} + {averageRating && {this.renderStars()}} + {repository && Repository} + {license && {license}} +
+
{description}
+ {this.renderAction()} +
+
+ {readme &&
this.body = (body || undefined)} + onClick={this.openLink} + dangerouslySetInnerHTML={{ __html: readme }} />} + ; + } + + protected renderNamespaceAccess(): React.ReactNode { + const { publisher, namespaceAccess, publishedBy } = this.props.extension; + if (namespaceAccess === undefined) { + return undefined; + } + let tooltip = publishedBy ? ` Published by "${publishedBy.loginName}".` : ''; + let icon; + if (namespaceAccess === 'public') { + icon = 'globe'; + tooltip = `Everyone can publish to "${publisher}" namespace.` + tooltip; + } else { + icon = 'shield'; + tooltip = `Only verified owners can publish to "${publisher}" namespace.` + tooltip; + } + return ; + } + + protected renderStars(): React.ReactNode { + const rating = this.props.extension.averageRating; + if (typeof rating !== 'number') { + return undefined; + } + const renderStarAt = (position: number) => position <= rating ? + : + position > rating && position - rating < 1 ? + : + ; + return + {renderStarAt(1)}{renderStarAt(2)}{renderStarAt(3)}{renderStarAt(4)}{renderStarAt(5)} + ; + } + + protected body: HTMLElement | undefined; + + // TODO replace with webview + readonly openLink = (event: React.MouseEvent) => { + if (!this.body) { + return; + } + const target = event.nativeEvent.target; + if (!(target instanceof HTMLElement)) { + return; + } + let node = target; + while (node.tagName.toLowerCase() !== 'a') { + if (node === this.body) { + return; + } + if (!(node.parentElement instanceof HTMLElement)) { + return; + } + node = node.parentElement; + } + const href = node.getAttribute('href'); + if (href && !href.startsWith('#')) { + event.preventDefault(); + this.props.extension.doOpen(new URI(href)); + } + }; + + readonly openExtension = async (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + const extension = this.props.extension; + const uri = await extension.environment.getRegistryUri(); + extension.doOpen(uri.resolve('extension/' + extension.id.replace('.', '/'))); + }; + readonly searchPublisher = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + const extension = this.props.extension; + if (extension.publisher) { + extension.search.query = extension.publisher; + } + }; + readonly openPublishedBy = async (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + const extension = this.props.extension; + const homepage = extension.publishedBy && extension.publishedBy.homepage; + if (homepage) { + extension.doOpen(new URI(homepage)); + } + }; + readonly openAverageRating = async (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + const extension = this.props.extension; + const uri = await extension.environment.getRegistryUri(); + extension.doOpen(uri.resolve('extension/' + extension.id.replace('.', '/') + '/reviews')); + }; + readonly openRepository = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + const extension = this.props.extension; + if (extension.repository) { + extension.doOpen(new URI(extension.repository)); + } + }; + readonly openLicense = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + const extension = this.props.extension; + const licenseUrl = extension.licenseUrl; + if (licenseUrl) { + extension.doOpen(new URI(licenseUrl)); + } + }; + +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts new file mode 100644 index 0000000000000..ab8663177efb4 --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts @@ -0,0 +1,104 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { Command, CommandRegistry } from '@theia/core/lib/common/command'; +import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; +import { VSXExtensionsViewContainer } from './vsx-extensions-view-container'; +import { Widget } from '@theia/core/lib/browser/widgets/widget'; +import { VSXExtensionsModel } from './vsx-extensions-model'; +import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; +import { ColorRegistry, Color } from '@theia/core/lib/browser/color-registry'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; + +export namespace VSXExtensionsCommands { + export const CLEAR_ALL: Command = { + id: 'vsxExtensions.clearAll', + category: 'Extensions', + label: 'Clear Search Results', + iconClass: 'clear-all' + }; +} + +@injectable() +export class VSXExtensionsContribution extends AbstractViewContribution implements ColorContribution, TabBarToolbarContribution { + + @inject(VSXExtensionsModel) + protected readonly model: VSXExtensionsModel; + + constructor() { + super({ + widgetId: VSXExtensionsViewContainer.ID, + widgetName: VSXExtensionsViewContainer.LABEL, + defaultWidgetOptions: { + area: 'left', + rank: 500 + }, + toggleCommandId: 'vsxExtensions.toggle', + toggleKeybinding: 'ctrlcmd+shift+x' + }); + } + + registerCommands(commands: CommandRegistry): void { + super.registerCommands(commands); + commands.registerCommand(VSXExtensionsCommands.CLEAR_ALL, { + execute: w => this.withWidget(w, () => this.model.search.query = ''), + isEnabled: w => this.withWidget(w, () => !!this.model.search.query), + isVisible: w => this.withWidget(w, () => true) + }); + } + + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: VSXExtensionsCommands.CLEAR_ALL.id, + command: VSXExtensionsCommands.CLEAR_ALL.id, + tooltip: VSXExtensionsCommands.CLEAR_ALL.label, + priority: 1, + onDidChange: this.model.onDidChange + }); + } + + registerColors(colors: ColorRegistry): void { + // VS Code colors should be aligned with https://code.visualstudio.com/api/references/theme-color#extensions + colors.register( + { + id: 'extensionButton.prominentBackground', defaults: { + dark: '#327e36', + light: '#327e36' + }, description: 'Button background color for actions extension that stand out (e.g. install button).' + }, + { + id: 'extensionButton.prominentForeground', defaults: { + dark: Color.white, + light: Color.white + }, description: 'Button foreground color for actions extension that stand out (e.g. install button).' + }, + { + id: 'extensionButton.prominentHoverBackground', defaults: { + dark: '#28632b', + light: '#28632b' + }, description: 'Button background hover color for actions extension that stand out (e.g. install button).' + } + ); + } + + protected withWidget(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: VSXExtensionsViewContainer) => T): T | false { + if (widget instanceof VSXExtensionsViewContainer && widget.id === VSXExtensionsViewContainer.ID) { + return fn(widget); + } + return false; + } +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-model.ts b/packages/vsx-registry/src/browser/vsx-extensions-model.ts new file mode 100644 index 0000000000000..c184f1b985d8c --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-model.ts @@ -0,0 +1,242 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct } from 'inversify'; +import debounce from 'p-debounce'; +import * as showdown from 'showdown'; +import * as sanitize from 'sanitize-html'; +import { Emitter } from '@theia/core/lib/common/event'; +import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation'; +import { VSXRegistryAPI, VSXResponseError } from '../common/vsx-registry-api'; +import { VSXSearchParam } from '../common/vsx-registry-types'; +import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { VSXExtension, VSXExtensionFactory } from './vsx-extension'; +import { ProgressService } from '@theia/core/lib/common/progress-service'; +import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; +import { Deferred } from '@theia/core/lib/common/promise-util'; + +@injectable() +export class VSXExtensionsModel { + + protected readonly onDidChangeEmitter = new Emitter(); + readonly onDidChange = this.onDidChangeEmitter.event; + + @inject(VSXRegistryAPI) + protected readonly api: VSXRegistryAPI; + + @inject(HostedPluginSupport) + protected readonly pluginSupport: HostedPluginSupport; + + @inject(VSXExtensionFactory) + protected readonly extensionFactory: VSXExtensionFactory; + + @inject(ProgressService) + protected readonly progressService: ProgressService; + + @inject(VSXExtensionsSearchModel) + readonly search: VSXExtensionsSearchModel; + + protected readonly initialized = new Deferred(); + + @postConstruct() + protected async init(): Promise { + await Promise.all([ + this.initInstalled(), + this.initSearchResult() + ]); + this.initialized.resolve(); + } + + protected async initInstalled(): Promise { + await this.pluginSupport.willStart; + this.pluginSupport.onDidChangePlugins(() => this.updateInstalled()); + try { + await this.updateInstalled(); + } catch (e) { + console.error(e); + } + } + + protected async initSearchResult(): Promise { + this.search.onDidChangeQuery(() => this.updateSearchResult()); + try { + await this.updateSearchResult(); + } catch (e) { + console.error(e); + } + } + + /** + * single source of all extensions + */ + protected readonly extensions = new Map(); + + protected _installed = new Set(); + get installed(): IterableIterator { + return this._installed.values(); + } + + protected _searchResult = new Set(); + get searchResult(): IterableIterator { + return this._searchResult.values(); + } + + getExtension(id: string): VSXExtension | undefined { + return this.extensions.get(id); + } + + protected setExtension(id: string): VSXExtension { + let extension = this.extensions.get(id); + if (!extension) { + extension = this.extensionFactory({ id }); + this.extensions.set(id, extension); + } + return extension; + } + + protected doChange(task: () => Promise): Promise; + protected doChange(task: () => Promise, token: CancellationToken): Promise; + protected doChange(task: () => Promise, token: CancellationToken = CancellationToken.None): Promise { + return this.progressService.withProgress('', 'extensions', async () => { + if (token && token.isCancellationRequested) { + return undefined; + } + const result = await task(); + if (token && token.isCancellationRequested) { + return undefined; + } + this.onDidChangeEmitter.fire(undefined); + 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 }, this.searchCancellationTokenSource.token); + }, 150); + protected doUpdateSearchResult(param: VSXSearchParam, token: CancellationToken): Promise { + return this.doChange(async () => { + const result = await this.api.search(param); + if (token.isCancellationRequested) { + return; + } + const searchResult = new Set(); + for (const data of result.extensions) { + const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase(); + this.setExtension(id).update(Object.assign(data, { + publisher: data.namespace, + downloadUrl: data.files.download, + iconUrl: data.files.icon, + readmeUrl: data.files.readme, + licenseUrl: data.files.license, + })); + searchResult.add(id); + } + this._searchResult = searchResult; + }, token); + } + + protected async updateInstalled(): Promise { + return this.doChange(async () => { + const plugins = this.pluginSupport.plugins; + const installed = new Set(); + const refreshing = []; + for (const plugin of plugins) { + if (plugin.model.engine.type === 'vscode') { + const id = plugin.model.id; + this._installed.delete(id); + const extension = this.setExtension(id); + installed.add(extension.id); + refreshing.push(this.refresh(id)); + } + } + for (const id of this._installed) { + refreshing.push(this.refresh(id)); + } + Promise.all(refreshing); + this._installed = installed; + }); + } + + 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 rawReadme = await this.api.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 markdownConverter = new showdown.Converter({ + noHeaderId: true, + strikethrough: true, + headerLevelStart: 2 + }); + + const readmeHtml = markdownConverter.makeHtml(readmeMarkdown); + return sanitize(readmeHtml, { + allowedTags: sanitize.defaults.allowedTags.concat(['h1', 'h2', 'img']) + }); + } + + protected async refresh(id: string): Promise { + try { + const data = await this.api.getExtension(id); + if (data.error) { + return this.onDidFailRefresh(id, data.error); + } + const extension = this.setExtension(id); + extension.update(Object.assign(data, { + publisher: data.namespace, + downloadUrl: data.files.download, + iconUrl: data.files.icon, + readmeUrl: data.files.readme, + licenseUrl: data.files.license, + })); + return extension; + } catch (e) { + return this.onDidFailRefresh(id, e); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected onDidFailRefresh(id: string, error: any): VSXExtension | undefined { + const cached = this.getExtension(id); + if (cached && cached.installed) { + return cached; + } + console.error(`[${id}]: failed to refresh, reason:`, error); + return undefined; + } + +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx b/packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx new file mode 100644 index 0000000000000..c9d88fd883eca --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-search-bar.tsx @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { injectable, postConstruct, inject } from 'inversify'; +import { ReactWidget, Message } from '@theia/core/lib/browser/widgets'; +import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; + +@injectable() +export class VSXExtensionsSearchBar extends ReactWidget { + + @inject(VSXExtensionsSearchModel) + protected readonly model: VSXExtensionsSearchModel; + + @postConstruct() + protected init(): void { + this.id = 'vsx-extensions-search-bar'; + this.addClass('theia-vsx-extensions-search-bar'); + this.model.onDidChangeQuery(() => this.update()); + } + + protected input: HTMLInputElement | undefined; + + protected render(): React.ReactNode { + return this.input = input || undefined} + value={this.model.query} + className='theia-input' + placeholder='Search Extensions in Open VSX Registry' + onChange={this.updateQuery}> + ; + } + + protected updateQuery = (e: React.ChangeEvent) => this.model.query = e.target.value; + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + if (this.input) { + this.input.focus(); + } + } + + protected onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); + this.update(); + } + +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-search-model.ts b/packages/vsx-registry/src/browser/vsx-extensions-search-model.ts new file mode 100644 index 0000000000000..84e175479dc1e --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-search-model.ts @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; + +@injectable() +export class VSXExtensionsSearchModel { + + protected readonly onDidChangeQueryEmitter = new Emitter(); + readonly onDidChangeQuery = this.onDidChangeQueryEmitter.event; + + protected _query = ''; + set query(query: string) { + if (this._query === query) { + return; + } + this._query = query; + this.onDidChangeQueryEmitter.fire(this._query); + } + get query(): string { + return this._query; + } + +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-source.ts b/packages/vsx-registry/src/browser/vsx-extensions-source.ts new file mode 100644 index 0000000000000..edfa90ac3b756 --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-source.ts @@ -0,0 +1,67 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct } from 'inversify'; +import { TreeSource, TreeElement } from '@theia/core/lib/browser/source-tree'; +import { VSXExtensionsModel } from './vsx-extensions-model'; + +@injectable() +export class VSXExtensionsSourceOptions { + static INSTALLED = 'installed'; + static BUITLT_IN = 'builtin'; + static SEARCH_RESULT = 'searchResult'; + readonly id: string; +} + +@injectable() +export class VSXExtensionsSource extends TreeSource { + + @inject(VSXExtensionsSourceOptions) + protected readonly options: VSXExtensionsSourceOptions; + + @inject(VSXExtensionsModel) + protected readonly model: VSXExtensionsModel; + + @postConstruct() + protected async init(): Promise { + this.fireDidChange(); + this.toDispose.push(this.model.onDidChange(() => this.fireDidChange())); + } + + *getElements(): IterableIterator { + for (const id of this.doGetElements()) { + const extension = this.model.getExtension(id); + if (!extension) { + continue; + } + if (this.options.id === VSXExtensionsSourceOptions.BUITLT_IN) { + if (extension.builtin) { + yield extension; + } + } else if (!extension.builtin) { + yield extension; + } + } + } + + protected doGetElements(): IterableIterator { + if (this.options.id === VSXExtensionsSourceOptions.SEARCH_RESULT) { + return this.model.searchResult; + } + return this.model.installed; + } + +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-view-container.ts b/packages/vsx-registry/src/browser/vsx-extensions-view-container.ts new file mode 100644 index 0000000000000..0192fef03ce2b --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-view-container.ts @@ -0,0 +1,143 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + *******************************************************************************‚*/ + +import { injectable, inject, postConstruct } from 'inversify'; +import { ViewContainer, PanelLayout, ViewContainerPart, Message } from '@theia/core/lib/browser'; +import { VSXExtensionsSearchBar } from './vsx-extensions-search-bar'; +import { VSXExtensionsWidget, } from './vsx-extensions-widget'; +import { VSXExtensionsModel } from './vsx-extensions-model'; + +@injectable() +export class VSXExtensionsViewContainer extends ViewContainer { + + static ID = 'vsx-extensions-view-container'; + static LABEL = 'Extensions'; + + @inject(VSXExtensionsSearchBar) + protected readonly searchBar: VSXExtensionsSearchBar; + + @inject(VSXExtensionsModel) + protected readonly model: VSXExtensionsModel; + + @postConstruct() + protected init(): void { + super.init(); + this.id = VSXExtensionsViewContainer.ID; + this.addClass('theia-vsx-extensions-view-container'); + + this.setTitleOptions({ + label: VSXExtensionsViewContainer.LABEL, + iconClass: 'theia-vsx-extensions-icon', + closeable: true + }); + } + + protected onActivateRequest(msg: Message): void { + this.searchBar.activate(); + } + + protected onAfterAttach(msg: Message): void { + super.onBeforeAttach(msg); + this.updateMode(); + this.toDisposeOnDetach.push(this.model.search.onDidChangeQuery(() => this.updateMode())); + } + + protected configureLayout(layout: PanelLayout): void { + layout.addWidget(this.searchBar); + super.configureLayout(layout); + } + + protected currentMode: VSXExtensionsViewContainer.Mode = VSXExtensionsViewContainer.InitialMode; + protected readonly lastModeState = new Map(); + + protected updateMode(): void { + const currentMode: VSXExtensionsViewContainer.Mode = !this.model.search.query ? VSXExtensionsViewContainer.DefaultMode : VSXExtensionsViewContainer.SearchResultMode; + if (currentMode === this.currentMode) { + return; + } + if (this.currentMode !== VSXExtensionsViewContainer.InitialMode) { + this.lastModeState.set(this.currentMode, super.doStoreState()); + } + this.currentMode = currentMode; + const lastState = this.lastModeState.get(currentMode); + if (lastState) { + super.doRestoreState(lastState); + } else { + for (const part of this.getParts()) { + this.applyModeToPart(part); + } + } + if (this.currentMode === VSXExtensionsViewContainer.SearchResultMode) { + const searchPart = this.getParts().find(part => part.wrapped.id === VSXExtensionsWidget.SEARCH_RESULT_ID); + if (searchPart) { + searchPart.collapsed = false; + searchPart.show(); + } + } + } + + protected registerPart(part: ViewContainerPart): void { + super.registerPart(part); + this.applyModeToPart(part); + } + + protected applyModeToPart(part: ViewContainerPart): void { + const partMode = (part.wrapped.id === VSXExtensionsWidget.SEARCH_RESULT_ID ? VSXExtensionsViewContainer.SearchResultMode : VSXExtensionsViewContainer.DefaultMode); + if (this.currentMode === partMode) { + part.show(); + } else { + part.hide(); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected doStoreState(): any { + const modes: VSXExtensionsViewContainer.State['modes'] = {}; + for (const mode of this.lastModeState.keys()) { + modes[mode] = this.lastModeState.get(mode); + } + return { + query: this.model.search.query, + modes + }; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected doRestoreState(state: any): void { + // eslint-disable-next-line guard-for-in + for (const key in state.modes) { + const mode = Number(key) as VSXExtensionsViewContainer.Mode; + const modeState = state.modes[mode]; + if (modeState) { + this.lastModeState.set(mode, modeState); + } + } + this.model.search.query = state.query; + } + +} +export namespace VSXExtensionsViewContainer { + export const InitialMode = 0; + export const DefaultMode = 1; + export const SearchResultMode = 2; + export type Mode = typeof InitialMode | typeof DefaultMode | typeof SearchResultMode; + export interface State { + query: string; + modes: { + [mode: number]: ViewContainer.State | undefined + } + } +} diff --git a/packages/vsx-registry/src/browser/vsx-extensions-widget.ts b/packages/vsx-registry/src/browser/vsx-extensions-widget.ts new file mode 100644 index 0000000000000..2e3a924e792d0 --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-extensions-widget.ts @@ -0,0 +1,86 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, interfaces, postConstruct, inject } from 'inversify'; +import { TreeNode, NodeProps } from '@theia/core/lib/browser/tree'; +import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree'; +import { VSXExtensionsSource, VSXExtensionsSourceOptions } from './vsx-extensions-source'; + +@injectable() +export class VSXExtensionsWidgetOptions extends VSXExtensionsSourceOptions { +} + +@injectable() +export class VSXExtensionsWidget extends SourceTreeWidget { + + static ID = 'vsx-extensions'; + static INSTALLED_ID = VSXExtensionsWidget.ID + ':' + VSXExtensionsSourceOptions.INSTALLED; + static SEARCH_RESULT_ID = VSXExtensionsWidget.ID + ':' + VSXExtensionsSourceOptions.SEARCH_RESULT; + static BUITLT_IN_ID = VSXExtensionsWidget.ID + ':' + VSXExtensionsSourceOptions.BUITLT_IN; + + static createWidget(parent: interfaces.Container, options: VSXExtensionsWidgetOptions): VSXExtensionsWidget { + const child = SourceTreeWidget.createContainer(parent, { + virtualized: false, + scrollIfActive: true + }); + child.bind(VSXExtensionsSourceOptions).toConstantValue(options); + child.bind(VSXExtensionsSource).toSelf(); + child.unbind(SourceTreeWidget); + child.bind(VSXExtensionsWidgetOptions).toConstantValue(options); + child.bind(VSXExtensionsWidget).toSelf(); + return child.get(VSXExtensionsWidget); + } + + @inject(VSXExtensionsWidgetOptions) + protected readonly options: VSXExtensionsWidgetOptions; + + @inject(VSXExtensionsSource) + protected readonly extensionsSource: VSXExtensionsSource; + + @postConstruct() + protected init(): void { + super.init(); + this.addClass('theia-vsx-extensions'); + + this.id = VSXExtensionsWidget.ID + ':' + this.options.id; + const title = this.computeTitle(); + this.title.label = title; + this.title.caption = title; + + this.toDispose.push(this.extensionsSource); + this.source = this.extensionsSource; + } + + protected computeTitle(): string { + if (this.id === VSXExtensionsWidget.INSTALLED_ID) { + return 'Installed'; + } + if (this.id === VSXExtensionsWidget.BUITLT_IN_ID) { + return 'Built-in'; + } + return 'Open VSX Registry'; + } + + protected getDefaultNodeStyle(node: TreeNode, props: NodeProps): React.CSSProperties | undefined { + const style = super.getDefaultNodeStyle(node, props); + if (style) { + style.paddingLeft = `${this.props.leftPadding}px`; + } + return style; + } + +} + diff --git a/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts new file mode 100644 index 0000000000000..9203708b08d63 --- /dev/null +++ b/packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import '../../src/browser/style/index.css'; + +import { ContainerModule } from 'inversify'; +import { WidgetFactory, bindViewContribution, FrontendApplicationContribution, ViewContainerIdentifier, OpenHandler, WidgetManager } from '@theia/core/lib/browser'; +import { VSXExtensionsViewContainer } from './vsx-extensions-view-container'; +import { VSXExtensionsContribution } from './vsx-extensions-contribution'; +import { VSXExtensionsSearchBar } from './vsx-extensions-search-bar'; +import { VSXRegistryAPI } from '../common/vsx-registry-api'; +import { VSXExtensionsModel } from './vsx-extensions-model'; +import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; +import { VSXExtensionsWidget, VSXExtensionsWidgetOptions } from './vsx-extensions-widget'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { VSXExtensionFactory, VSXExtension, VSXExtensionOptions } from './vsx-extension'; +import { VSXExtensionEditor } from './vsx-extension-editor'; +import { VSXExtensionEditorManager } from './vsx-extension-editor-manager'; +import { VSXExtensionsSourceOptions } from './vsx-extensions-source'; +import { VSXEnvironment } from '../common/vsx-environment'; +import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; + +export default new ContainerModule(bind => { + bind(VSXEnvironment).toSelf().inRequestScope(); + bind(VSXRegistryAPI).toSelf().inSingletonScope(); + + bind(VSXExtension).toSelf(); + bind(VSXExtensionFactory).toFactory(ctx => (option: VSXExtensionOptions) => { + const child = ctx.container.createChild(); + child.bind(VSXExtensionOptions).toConstantValue(option); + return child.get(VSXExtension); + }); + bind(VSXExtensionsModel).toSelf().inSingletonScope(); + + bind(VSXExtensionEditor).toSelf(); + bind(WidgetFactory).toDynamicValue(ctx => ({ + id: VSXExtensionEditor.ID, + createWidget: async (options: VSXExtensionOptions) => { + const extension = await ctx.container.get(VSXExtensionsModel).resolve(options.id); + const child = ctx.container.createChild(); + child.bind(VSXExtension).toConstantValue(extension); + return child.get(VSXExtensionEditor); + } + })).inSingletonScope(); + bind(VSXExtensionEditorManager).toSelf().inSingletonScope(); + bind(OpenHandler).toService(VSXExtensionEditorManager); + + bind(WidgetFactory).toDynamicValue(({ container }) => ({ + id: VSXExtensionsWidget.ID, + createWidget: async (options: VSXExtensionsWidgetOptions) => VSXExtensionsWidget.createWidget(container, options) + })).inSingletonScope(); + bind(WidgetFactory).toDynamicValue(ctx => ({ + id: VSXExtensionsViewContainer.ID, + createWidget: async () => { + const child = ctx.container.createChild(); + child.bind(ViewContainerIdentifier).toConstantValue({ + id: VSXExtensionsViewContainer.ID, + progressLocationId: 'extensions' + }); + child.bind(VSXExtensionsViewContainer).toSelf(); + const viewContainer = child.get(VSXExtensionsViewContainer); + const widgetManager = child.get(WidgetManager); + for (const id of [VSXExtensionsSourceOptions.SEARCH_RESULT, VSXExtensionsSourceOptions.INSTALLED, VSXExtensionsSourceOptions.BUITLT_IN]) { + const widget = await widgetManager.getOrCreateWidget(VSXExtensionsWidget.ID, { id }); + viewContainer.addWidget(widget, { + initiallyCollapsed: id === VSXExtensionsSourceOptions.BUITLT_IN + }); + } + return viewContainer; + } + })).inSingletonScope(); + + bind(VSXExtensionsSearchModel).toSelf().inSingletonScope(); + bind(VSXExtensionsSearchBar).toSelf().inSingletonScope(); + + bindViewContribution(bind, VSXExtensionsContribution); + bind(FrontendApplicationContribution).toService(VSXExtensionsContribution); + bind(ColorContribution).toService(VSXExtensionsContribution); + bind(TabBarToolbarContribution).toService(VSXExtensionsContribution); +}); diff --git a/packages/vsx-registry/src/common/vsx-environment.tsx b/packages/vsx-registry/src/common/vsx-environment.tsx new file mode 100644 index 0000000000000..2bc67383957b6 --- /dev/null +++ b/packages/vsx-registry/src/common/vsx-environment.tsx @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import URI from '@theia/core/lib/common/uri'; + +@injectable() +export class VSXEnvironment { + + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; + + protected _registryUri: URI | undefined; + async getRegistryUri(): Promise { + if (!this._registryUri) { + const vsxRegistryUrl = await this.environments.getValue('VSX_REGISTRY_URL'); + this._registryUri = new URI(vsxRegistryUrl && vsxRegistryUrl.value || 'https://open-vsx.org'); + } + return this._registryUri; + } + + async getRegistryApiUri(): Promise { + const registryUri = await this.getRegistryUri(); + return registryUri.resolve('api'); + } + +} diff --git a/packages/vsx-registry/src/common/vsx-extension-uri.ts b/packages/vsx-registry/src/common/vsx-extension-uri.ts new file mode 100644 index 0000000000000..b9033f49d9ceb --- /dev/null +++ b/packages/vsx-registry/src/common/vsx-extension-uri.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import URI from '@theia/core/lib/common/uri'; + +export namespace VSXExtensionUri { + export function toUri(id: string): URI { + return new URI(`vscode:extension/${id}`); + } + export function toId(uri: URI): string | undefined { + if (uri.scheme === 'vscode' && uri.path.dir.toString() === 'extension') { + return uri.path.base; + } + return undefined; + } +} diff --git a/packages/vsx-registry/src/common/vsx-registry-api.ts b/packages/vsx-registry/src/common/vsx-registry-api.ts new file mode 100644 index 0000000000000..8ecaa3bfb2230 --- /dev/null +++ b/packages/vsx-registry/src/common/vsx-registry-api.ts @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as bent from 'bent'; +import { injectable, inject } from 'inversify'; +import { VSXExtensionRaw, VSXSearchParam, VSXSearchResult } from './vsx-registry-types'; +import { VSXEnvironment } from './vsx-environment'; + +const fetchText = bent('GET', 'string', 200); +const fetchJson = bent('GET', 'json', 200); + +export interface VSXResponseError extends Error { + statusCode: number +} +export namespace VSXResponseError { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + export function is(error: any): error is VSXResponseError { + return !!error && typeof error === 'object' + && 'statusCode' in error && typeof error['statusCode'] === 'number'; + } +} + +@injectable() +export class VSXRegistryAPI { + + @inject(VSXEnvironment) + protected readonly environment: VSXEnvironment; + + async search(param?: VSXSearchParam): Promise { + const apiUri = await this.environment.getRegistryApiUri(); + let searchUri = apiUri.resolve('-/search').toString(); + if (param) { + let query = ''; + if (param.query) { + query += 'query=' + encodeURIComponent(param.query); + } + if (param.category) { + query += 'category=' + encodeURIComponent(param.category); + } + if (param.size) { + query += 'size=' + param.size; + } + if (param.offset) { + query += 'offset=' + param.offset; + } + if (query) { + searchUri += '?' + query; + } + } + return this.fetchJson(searchUri); + } + + async getExtension(id: string): Promise { + const apiUri = await this.environment.getRegistryApiUri(); + return this.fetchJson(apiUri.resolve(id.replace('.', '/')).toString()); + } + + protected async fetchJson(url: string): Promise { + const result = await fetchJson(url); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return result as any as T; + } + + fetchText(url: string): Promise { + return fetchText(url); + } + +} diff --git a/packages/vsx-registry/src/common/vsx-registry-types.ts b/packages/vsx-registry/src/common/vsx-registry-types.ts new file mode 100644 index 0000000000000..ef96dcf859cb5 --- /dev/null +++ b/packages/vsx-registry/src/common/vsx-registry-types.ts @@ -0,0 +1,105 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/** + * Should be aligned with https://github.com/eclipse/openvsx/blob/793d0691258a6029e5ebb8cc8783b366b67d16ca/server/src/main/java/org/eclipse/openvsx/RegistryAPI.java#L192-L196 + */ +export interface VSXSearchParam { + query?: string; + category?: string; + size?: number; + offset?: number; +} + +/** + * 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[]; +} + +/** + * 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 + 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; +} + +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 +} + +/** + * 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: { + download: string + readme?: string + license?: string + icon?: string + } + 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; +} diff --git a/packages/vsx-registry/src/node/vsx-extension-resolver.ts b/packages/vsx-registry/src/node/vsx-extension-resolver.ts new file mode 100644 index 0000000000000..6a81bb5a886a1 --- /dev/null +++ b/packages/vsx-registry/src/node/vsx-extension-resolver.ts @@ -0,0 +1,90 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { v4 as uuidv4 } from 'uuid'; +import * as requestretry from 'requestretry'; +import { injectable, inject } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { PluginDeployerResolver, PluginDeployerResolverContext } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { VSXExtensionUri } from '../common/vsx-extension-uri'; +import { VSXRegistryAPI } from '../common/vsx-registry-api'; + +@injectable() +export class VSXExtensionResolver implements PluginDeployerResolver { + + @inject(VSXRegistryAPI) + protected readonly api: VSXRegistryAPI; + + protected readonly downloadPath: string; + + constructor() { + this.downloadPath = path.resolve(os.tmpdir(), uuidv4()); + fs.ensureDirSync(this.downloadPath); + fs.emptyDirSync(this.downloadPath); + } + + accept(pluginId: string): boolean { + return !!VSXExtensionUri.toId(new URI(pluginId)); + } + + async resolve(context: PluginDeployerResolverContext): Promise { + const id = VSXExtensionUri.toId(new URI(context.getOriginId())); + if (!id) { + return; + } + console.log(`[${id}]: trying to resolve latest version...`); + const extension = await this.api.getExtension(id); + if (extension.error) { + throw new Error(extension.error); + } + const resolvedId = id + '-' + extension.version; + const downloadUrl = extension.files.download; + console.log(`[${id}]: resolved to '${resolvedId}'`); + + const extensionPath = path.resolve(this.downloadPath, path.basename(downloadUrl)); + console.log(`[${resolvedId}]: trying to download from "${downloadUrl}"...`); + if (!await this.download(downloadUrl, extensionPath)) { + console.log(`[${resolvedId}]: not found`); + return; + } + console.log(`[${resolvedId}]: downloaded to ${extensionPath}"`); + context.addPlugin(resolvedId, extensionPath); + } + + protected async download(downloadUrl: string, downloadPath: string): Promise { + return new Promise((resolve, reject) => { + requestretry(downloadUrl, { + method: 'GET', + maxAttempts: 5, + retryDelay: 2000, + retryStrategy: requestretry.RetryStrategies.HTTPOrNetworkError + }, (err, response) => { + if (err) { + reject(err); + } else if (response && response.statusCode === 404) { + resolve(false); + } else if (response && response.statusCode !== 200) { + reject(new Error(response.statusMessage)); + } + }).pipe(fs.createWriteStream(downloadPath)) + .on('error', reject) + .on('close', () => resolve(true)); + }); + } +} diff --git a/packages/vsx-registry/src/node/vsx-registry-backend-module.ts b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts new file mode 100644 index 0000000000000..a084431921fd7 --- /dev/null +++ b/packages/vsx-registry/src/node/vsx-registry-backend-module.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { VSXExtensionResolver } from './vsx-extension-resolver'; +import { PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { VSXRegistryAPI } from '../common/vsx-registry-api'; +import { VSXEnvironment } from '../common/vsx-environment'; + +export default new ContainerModule(bind => { + bind(VSXEnvironment).toSelf().inRequestScope(); + bind(VSXRegistryAPI).toSelf().inSingletonScope(); + + bind(VSXExtensionResolver).toSelf().inSingletonScope(); + bind(PluginDeployerResolver).toService(VSXExtensionResolver); +}); diff --git a/packages/vsx-registry/src/package.spec.ts b/packages/vsx-registry/src/package.spec.ts new file mode 100644 index 0000000000000..109b20cb7683a --- /dev/null +++ b/packages/vsx-registry/src/package.spec.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('vsx-registry package', () => { + + it('support code coverage statistics', () => true); + +}); diff --git a/tsconfig.json b/tsconfig.json index 63b84501a0df6..e62e4145a55a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -159,6 +159,9 @@ ], "@theia/example-electron/*": [ "examples/electron/*" + ], + "@theia/vsx-registry/lib/*": [ + "packages/vsx-registry/src/*" ] } } diff --git a/yarn.lock b/yarn.lock index dd0bb2ae4521a..7c5a340d17860 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1061,6 +1061,13 @@ resolved "https://registry.yarnpkg.com/@types/base64-arraybuffer/-/base64-arraybuffer-0.1.0.tgz#739eea0a974d13ae831f96d97d882ceb0b187543" integrity sha512-oyV0CGER7tX6OlfnLfGze0XbsA7tfRuTtsQ2JbP8K5KBUzc24yoYRD+0XjMRQgOejvZWeIbtkNaHlE8akzj4aQ== +"@types/bent@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@types/bent/-/bent-7.0.1.tgz#fc798878190b8748650b7039fdf2d6e769025009" + integrity sha512-WgkridLPfbgdgCavp70c6vFR3cLV2n8hoyAG+xZjouiADpN6niow/OzszH02cRtW25dYoJnEaGWQ8siMO7Bh0Q== + dependencies: + "@types/node" "*" + "@types/body-parser@*", "@types/body-parser@^1.16.4", "@types/body-parser@^1.17.0": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897" @@ -1115,6 +1122,18 @@ resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.5.3.tgz#7c6c3721ba454d838790100faf7957116ee7deab" integrity sha512-YrLagYnL+tfrgM7bQ5yW34pi5cg9pmh5Gbq2Lmuuh+zh0ZjmK2fU3896PtlpJT3IDG2rdkoG30biHJepgIsMnw== +"@types/domhandler@*": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/domhandler/-/domhandler-2.4.1.tgz#7b3b347f7762180fbcb1ece1ce3dd0ebbb8c64cf" + integrity sha512-cfBw6q6tT5sa1gSPFSRKzF/xxYrrmeiut7E0TxNBObiLSBTuFEHibcfEe3waQPEDbqBsq+ql/TOniw65EyDFMA== + +"@types/domutils@*": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@types/domutils/-/domutils-1.7.2.tgz#89422e579c165994ad5c09ce90325da596cc105d" + integrity sha512-Nnwy1Ztwq42SSNSZSh9EXBJGrOZPR+PQ2sRT4VZy8hnsFXfCil7YlKO2hd2360HyrtFz2qwnKQ13ENrgXNxJbw== + dependencies: + "@types/domhandler" "*" + "@types/escape-html@^0.0.20": version "0.0.20" resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a" @@ -1168,6 +1187,15 @@ resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca" integrity sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ== +"@types/htmlparser2@*": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@types/htmlparser2/-/htmlparser2-3.10.1.tgz#1e65ba81401d53f425c1e2ba5a3d05c90ab742c7" + integrity sha512-fCxmHS4ryCUCfV9+CJZY1UjkbR+6Al/EQdX5Jh03qBj9gdlPG5q+7uNoDgE/ZNXb3XNWSAQgqKIWnbRCbOyyWA== + dependencies: + "@types/domhandler" "*" + "@types/domutils" "*" + "@types/node" "*" + "@types/jsdom@^11.0.4": version "11.12.0" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-11.12.0.tgz#00ddc6f0a1b04c2f5ff6fb23eb59360ca65f12ae" @@ -1355,6 +1383,13 @@ resolved "https://registry.yarnpkg.com/@types/route-parser/-/route-parser-0.1.3.tgz#f8af16886ebe0b525879628c04f81433ac617af0" integrity sha512-1AQYpsMbxangSnApsyIHzck5TP8cfas8fzmemljLi2APssJvlZiHkTar/ZtcZwOtK/Ory/xwLg2X8dwhkbnM+g== +"@types/sanitize-html@^1.13.31": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.20.2.tgz#59777f79f015321334e3a9f28882f58c0a0d42b8" + integrity sha512-SrefiiBebGIhxEFkpbbYOwO1S6+zQLWAC4s4tipchlHq1aO9bp0xiapM7Zm0ml20MF+3OePWYdksB1xtneKPxg== + dependencies: + "@types/htmlparser2" "*" + "@types/semver@^5.4.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" @@ -1368,6 +1403,11 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/showdown@^1.7.1": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@types/showdown/-/showdown-1.9.3.tgz#eaa881b03a32d3720184731754d3025fc450b970" + integrity sha512-akvzSmrvY4J5d3tHzUUiQr0xpjd4Nb3uzWW6dtwzYJ+qW/KdWw5F8NLatnor5q/1LURHnzDA1ReEwCVqcatRnw== + "@types/sinon@^2.3.5": version "2.3.7" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.7.tgz#e92c2fed3297eae078d78d1da032b26788b4af86" @@ -2877,6 +2917,15 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bent@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/bent/-/bent-7.1.0.tgz#4d1ee379259e40ef39e4a8f80fc97ab07f43185b" + integrity sha512-oTkDz8lBsk+HQjJPVeHHejkse/PDaTFwTFVasoR1V3XEQw23KKdfNvRln3M9++qipIBObqVyhsvSGuquFebDvQ== + dependencies: + bytesish "^0.4.1" + caseless "~0.12.0" + is-stream "^2.0.0" + big-integer@^1.6.17: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" @@ -3200,6 +3249,11 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytesish@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.1.tgz#5fe19b076037ffdfb63e083a53495b1d1c063f6f" + integrity sha512-j3l5QmnAbpOfcN/Z2Jcv4poQYfefs8rDdcbc6iEKm+OolvUXAE2APodpWj+DOzqX6Bl5Ys1cQkcIV2/doGvQxg== + cacache@^10.0.4: version "10.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" @@ -4755,11 +4809,24 @@ dom-helpers@^5.0.0: "@babel/runtime" "^7.6.3" csstype "^2.6.7" +dom-serializer@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -4767,6 +4834,22 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" +domhandler@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9" + integrity sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw== + dependencies: + domelementtype "^2.0.1" + +domutils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.0.0.tgz#15b8278e37bfa8468d157478c58c367718133c08" + integrity sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg== + dependencies: + dom-serializer "^0.2.1" + domelementtype "^2.0.1" + domhandler "^3.0.0" + dot-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" @@ -4999,6 +5082,11 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" +entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" + integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -6622,6 +6710,16 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== +htmlparser2@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" + integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== + dependencies: + domelementtype "^2.0.1" + domhandler "^3.0.0" + domutils "^2.0.0" + entities "^2.0.0" + http-cache-semantics@3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" @@ -7993,6 +8091,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -8008,6 +8111,16 @@ lodash.isinteger@^4.0.4: resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.isundefined@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" @@ -8018,6 +8131,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.mergewith@^4.6.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9988,6 +10106,15 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^7.0.27: + version "7.0.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" + integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + prebuild-install@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-4.0.0.tgz#206ce8106ce5efa4b6cf062fc8a0a7d93c17f3a8" @@ -11036,6 +11163,22 @@ samsam@1.x, samsam@^1.1.3: resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" integrity sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg== +sanitize-html@^1.14.1: + version "1.22.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.22.0.tgz#9df779c53cf5755adb2322943c21c1c1dffca7bf" + integrity sha512-3RPo65mbTKpOAdAYWU496MSty1YbB3Y5bjwL5OclgaSSMtv65xvM7RW/EHRumzaZ1UddEJowCbSdK0xl5sAu0A== + dependencies: + chalk "^2.4.1" + htmlparser2 "^4.1.0" + lodash.clonedeep "^4.5.0" + lodash.escaperegexp "^4.1.2" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.mergewith "^4.6.1" + postcss "^7.0.27" + srcset "^2.0.1" + xtend "^4.0.1" + sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -11219,6 +11362,13 @@ shelljs@^0.8.0, shelljs@^0.8.3: interpret "^1.0.0" rechoir "^0.6.2" +showdown@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.9.1.tgz#134e148e75cd4623e09c21b0511977d79b5ad0ef" + integrity sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA== + dependencies: + yargs "^14.2" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -11486,6 +11636,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +srcset@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac" + integrity sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ== + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -11827,6 +11982,13 @@ supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" @@ -13174,7 +13336,7 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xtend@^4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -13229,6 +13391,14 @@ yargs-parser@13.1.1, yargs-parser@^13.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08" + integrity sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^16.1.0: version "16.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1" @@ -13294,6 +13464,23 @@ yargs@^11.0.0, yargs@^11.1.0: y18n "^3.2.1" yargs-parser "^9.0.2" +yargs@^14.2: + version "14.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.2.tgz#2769564379009ff8597cdd38fba09da9b493c4b5" + integrity sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA== + dependencies: + cliui "^5.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^15.0.0" + yargs@^15.0.2, yargs@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.1.0.tgz#e111381f5830e863a89550bd4b136bb6a5f37219"