+
+ {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"