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/core/src/browser/view-container.ts b/packages/core/src/browser/view-container.ts index c5e8f6a789405..ed509d86b6470 100644 --- a/packages/core/src/browser/view-container.ts +++ b/packages/core/src/browser/view-container.ts @@ -18,10 +18,9 @@ import { interfaces, injectable, inject, postConstruct } from 'inversify'; import { IIterator, toArray, find, some, every, map } from '@phosphor/algorithm'; import { Widget, EXPANSION_TOGGLE_CLASS, COLLAPSED_CLASS, MessageLoop, Message, SplitPanel, BaseWidget, - addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener + addEventListener, SplitLayout, LayoutItem, PanelLayout, addKeyListener, waitForRevealed } from './widgets'; import { Event, Emitter } from '../common/event'; -import { Deferred } from '../common/promise-util'; import { Disposable, DisposableCollection } from '../common/disposable'; import { CommandRegistry } from '../common/command'; import { MenuModelRegistry, MenuPath, MenuAction } from '../common/menu'; @@ -57,7 +56,6 @@ export class ViewContainerIdentifier { export class ViewContainer extends BaseWidget implements StatefulWidget, ApplicationShell.TrackableWidgetProvider { protected panel: SplitPanel; - protected attached = new Deferred(); protected currentPart: ViewContainerPart | undefined; @@ -110,7 +108,7 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica }, this.splitPositionHandler) }); this.panel.node.tabIndex = -1; - layout.addWidget(this.panel); + this.configureLayout(layout); const { commandRegistry, menuRegistry, contextMenuRenderer } = this; this.toDispose.pushAll([ @@ -148,6 +146,10 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica } } + protected configureLayout(layout: PanelLayout): void { + layout.addWidget(this.panel); + } + protected readonly toDisposeOnCurrentPart = new DisposableCollection(); protected updateCurrentPart(part?: ViewContainerPart): void { @@ -345,6 +347,9 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica if (!this.isVisible && this.lastVisibleState) { return this.lastVisibleState; } + return this.doStoreState(); + } + protected doStoreState(): ViewContainer.State { const parts = this.getParts(); const availableSize = this.containerLayout.getAvailableSize(); const orientation = this.orientation; @@ -371,6 +376,9 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica */ restoreState(state: ViewContainer.State): void { this.lastVisibleState = state; + this.doRestoreState(state); + } + protected doRestoreState(state: ViewContainer.State): void { this.setTitleOptions(state.title); // restore widgets for (const part of state.parts) { @@ -406,7 +414,7 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica } // Restore part sizes - this.attached.promise.then(() => { + waitForRevealed(this).then(() => { this.containerLayout.setPartSizes(partStates.map(partState => partState.relativeSize)); }); } @@ -539,7 +547,6 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica } } super.onAfterAttach(msg); - requestAnimationFrame(() => this.attached.resolve()); } protected onBeforeHide(msg: Message): void { diff --git a/packages/plugin-ext-vscode/package.json b/packages/plugin-ext-vscode/package.json index ad321267550b1..afb04306528d8 100644 --- a/packages/plugin-ext-vscode/package.json +++ b/packages/plugin-ext-vscode/package.json @@ -10,6 +10,7 @@ "@theia/plugin-ext": "^0.16.0", "@theia/workspace": "^0.16.0", "@types/request": "^2.0.3", + "filenamify": "^4.1.0", "request": "^2.82.0" }, "publishConfig": { diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts index 0536077c5043e..0bce5c3e799c3 100644 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts @@ -17,8 +17,10 @@ import { ContainerModule } from 'inversify'; import { CommandContribution } from '@theia/core'; import { PluginVscodeCommandsContribution } from './plugin-vscode-commands-contribution'; +import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; export default new ContainerModule(bind => { + bind(PluginVSCodeEnvironment).toSelf().inSingletonScope(); bind(PluginVscodeCommandsContribution).toSelf().inSingletonScope(); bind(CommandContribution).toDynamicValue(context => context.container.get(PluginVscodeCommandsContribution)); }); diff --git a/packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts b/packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts new file mode 100644 index 0000000000000..56d0723de801b --- /dev/null +++ b/packages/plugin-ext-vscode/src/common/plugin-vscode-environment.ts @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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 PluginVSCodeEnvironment { + + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; + + protected _extensionsDirUri: URI | undefined; + async getExtensionsDirUri(): Promise { + if (!this._extensionsDirUri) { + const configDir = new URI(await this.environments.getConfigDirUri()); + this._extensionsDirUri = configDir.resolve('extensions'); + } + return this._extensionsDirUri; + } + +} diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-backend-module.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-backend-module.ts index 7bc4cb01f6e2d..1b39a16af6a09 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-backend-module.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-backend-module.ts @@ -15,23 +15,29 @@ ********************************************************************************/ import { ContainerModule } from 'inversify'; -import { PluginDeployerFileHandler, PluginDeployerDirectoryHandler, PluginScanner, PluginDeployerResolver } from '@theia/plugin-ext'; +import { + PluginDeployerFileHandler, PluginDeployerDirectoryHandler, PluginScanner, PluginDeployerParticipant +} from '@theia/plugin-ext'; import { PluginVsCodeFileHandler } from './plugin-vscode-file-handler'; import { PluginVsCodeDirectoryHandler } from './plugin-vscode-directory-handler'; import { VsCodePluginScanner } from './scanner-vscode'; -import { VsCodePluginDeployerResolver } from './plugin-vscode-resolver'; import { PluginVsCodeCliContribution } from './plugin-vscode-cli-contribution'; import { CliContribution } from '@theia/core/lib/node'; import { PluginHostEnvironmentVariable } from '@theia/plugin-ext/lib/common'; +import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; +import { PluginVSCodeDeployerParticipant } from './plugin-vscode-deployer-participant'; export default new ContainerModule(bind => { + bind(PluginVSCodeEnvironment).toSelf().inSingletonScope(); + + bind(PluginVSCodeDeployerParticipant).toSelf().inSingletonScope(); + bind(PluginDeployerParticipant).toService(PluginVSCodeDeployerParticipant); + bind(PluginDeployerFileHandler).to(PluginVsCodeFileHandler).inSingletonScope(); bind(PluginDeployerDirectoryHandler).to(PluginVsCodeDirectoryHandler).inSingletonScope(); bind(PluginScanner).to(VsCodePluginScanner).inSingletonScope(); - bind(PluginDeployerResolver).to(VsCodePluginDeployerResolver).inSingletonScope(); bind(PluginVsCodeCliContribution).toSelf().inSingletonScope(); bind(CliContribution).toService(PluginVsCodeCliContribution); bind(PluginHostEnvironmentVariable).toService(PluginVsCodeCliContribution); -} -); +}); diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts new file mode 100644 index 0000000000000..0b65c3f7c2171 --- /dev/null +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-deployer-participant.ts @@ -0,0 +1,32 @@ +/******************************************************************************** + * 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 { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; +import { PluginDeployerParticipant, PluginDeployerStartContext } from '@theia/plugin-ext/lib/common/plugin-protocol'; + +@injectable() +export class PluginVSCodeDeployerParticipant implements PluginDeployerParticipant { + + @inject(PluginVSCodeEnvironment) + protected readonly environments: PluginVSCodeEnvironment; + + async onWillStart(context: PluginDeployerStartContext): Promise { + const extensionsDirUri = await this.environments.getExtensionsDirUri(); + context.userEntries.push(extensionsDirUri.withScheme('local-dir').toString()); + } + +} diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts index 2731bc592a4a6..bd48d8a561643 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-directory-handler.ts @@ -67,6 +67,7 @@ export class PluginVsCodeDirectoryHandler implements PluginDeployerDirectoryHand } if (options) { plugin.storeValue('package.json', pck); + plugin.rootPath = plugin.path(); plugin.updatePath(pluginPath); } console.log(`Resolved "${plugin.id()}" to a VS Code extension "${pck.name}@${pck.version}" with engines:`, pck.engines); diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts index fca18ea8682ac..118ff0679ef98 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts @@ -14,19 +14,22 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext } from '@theia/plugin-ext'; -import { injectable } from 'inversify'; +import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext, PluginType } from '@theia/plugin-ext'; import * as fs from 'fs-extra'; import * as path from 'path'; +import * as filenamify from 'filenamify'; +import { injectable, inject } from 'inversify'; import { getTempDir } from '@theia/plugin-ext/lib/main/node/temp-dir-util'; +import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; +import { FileUri } from '@theia/core/lib/node/file-uri'; @injectable() export class PluginVsCodeFileHandler implements PluginDeployerFileHandler { - private unpackedFolder: string; - constructor() { - this.unpackedFolder = getTempDir('vscode-unpacked'); - } + @inject(PluginVSCodeEnvironment) + protected readonly environment: PluginVSCodeEnvironment; + + private readonly systemExtensionsDirUri = FileUri.create(getTempDir('vscode-unpacked')); accept(resolvedPlugin: PluginDeployerEntry): boolean { if (!resolvedPlugin.isFile()) { @@ -37,19 +40,36 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler { } async handle(context: PluginDeployerFileHandlerContext): Promise { - const unpackedPath = path.resolve(this.unpackedFolder, path.basename(context.pluginEntry().path())); - console.log(`unzipping the VS Code extension '${path.basename(context.pluginEntry().path())}' to directory: ${unpackedPath}`); + const id = context.pluginEntry().id(); + const extensionDir = await this.getExtensionDir(context); + console.log(`[${id}]: trying to decompress into "${extensionDir}"...`); + if (context.pluginEntry().type === PluginType.User && await fs.pathExists(extensionDir)) { + console.log(`[${id}]: already found`); + context.pluginEntry().updatePath(extensionDir); + return; + } + await this.decompress(extensionDir, context); + console.log(`[${id}]: decompressed`); + context.pluginEntry().updatePath(extensionDir); + } - await context.unzip(context.pluginEntry().path(), unpackedPath); + protected async getExtensionDir(context: PluginDeployerFileHandlerContext): Promise { + let extensionsDirUri = this.systemExtensionsDirUri; + if (context.pluginEntry().type === PluginType.User) { + extensionsDirUri = await this.environment.getExtensionsDirUri(); + } + return FileUri.fsPath(extensionsDirUri.resolve(filenamify(context.pluginEntry().id()))); + } + + protected async decompress(extensionDir: string, context: PluginDeployerFileHandlerContext): Promise { + await context.unzip(context.pluginEntry().path(), extensionDir); if (context.pluginEntry().path().endsWith('.tgz')) { - const extensionPath = path.join(unpackedPath, 'package'); + const extensionPath = path.join(extensionDir, 'package'); const vscodeNodeModulesPath = path.join(extensionPath, 'vscode_node_modules.zip'); if (await fs.pathExists(vscodeNodeModulesPath)) { await context.unzip(vscodeNodeModulesPath, path.join(extensionPath, 'node_modules')); } } - - context.pluginEntry().updatePath(unpackedPath); - return Promise.resolve(); } + } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-resolver.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-resolver.ts deleted file mode 100644 index 0c84a5a304b6f..0000000000000 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-resolver.ts +++ /dev/null @@ -1,137 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. 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 - ********************************************************************************/ - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { PluginDeployerResolver, PluginDeployerResolverContext } from '@theia/plugin-ext'; -import { injectable } from 'inversify'; -import * as request from 'request'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; - -/** - * Resolver that handle the vscode: protocol - */ -@injectable() -export class VsCodePluginDeployerResolver implements PluginDeployerResolver { - - private static PREFIX_VSCODE_EXTENSION = 'vscode:extension/'; - - private static PREFIX_EXT_INSTALL = 'ext install '; - - private static MARKET_PLACE_ENDPOINT = 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery'; - - private static HEADERS = { - 'Content-Type': 'application/json', - 'Accept': 'application/json;api-version=3.0-preview.1' - }; - - private vscodeExtensionsFolder: string; - constructor() { - this.vscodeExtensionsFolder = process.env.VSCODE_PLUGINS || path.resolve(os.tmpdir(), 'vscode-extension-marketplace'); - if (!fs.existsSync(this.vscodeExtensionsFolder)) { - fs.mkdirSync(this.vscodeExtensionsFolder); - } - } - - /** - * Download vscode extensions if available and add them as plugins. - */ - async resolve(pluginResolverContext: PluginDeployerResolverContext): Promise { - - // download the file - return new Promise((resolve, reject) => { - const originId = pluginResolverContext.getOriginId(); - - let extensionName = ''; - if (originId.startsWith(VsCodePluginDeployerResolver.PREFIX_VSCODE_EXTENSION)) { - extensionName = originId.substring(VsCodePluginDeployerResolver.PREFIX_VSCODE_EXTENSION.length); - } else if (originId.startsWith(VsCodePluginDeployerResolver.PREFIX_EXT_INSTALL)) { - extensionName = originId.substring(VsCodePluginDeployerResolver.PREFIX_EXT_INSTALL.length); - } - - if (!extensionName) { - reject(new Error('Invalid extension' + originId)); - return; - } - - const wantedExtensionVersion = undefined; - - const json = { - 'filters': [{ - 'criteria': [{ 'filterType': 7, 'value': extensionName }], 'pageNumber': 1, - 'pageSize': 1, 'sortBy': 0, 'sortOrder': 0 - }], 'assetTypes': ['Microsoft.VisualStudio.Services.VSIXPackage'], - 'flags': 131 - }; - - const options = { - url: VsCodePluginDeployerResolver.MARKET_PLACE_ENDPOINT, - headers: VsCodePluginDeployerResolver.HEADERS, - method: 'POST', - json: json - }; - - request(options, (error, response, body) => { - if (error) { - reject(error); - return; - } else if (response.statusCode === 200) { - const extension = body.results[0].extensions[0]; - if (!extension) { - reject(new Error('No extension')); - return; - } - let asset; - if (wantedExtensionVersion !== undefined) { - const extensionVersion = extension.versions.filter((value: any) => value.version === wantedExtensionVersion)[0]; - asset = extensionVersion.files.filter((f: any) => f.assetType === 'Microsoft.VisualStudio.Services.VSIXPackage')[0]; - } else { - // take first one - asset = extension.versions[0].files.filter((f: any) => f.assetType === 'Microsoft.VisualStudio.Services.VSIXPackage')[0]; - } - - const shortName = extensionName.replace(/\W/g, '_'); - const extensionPath = path.resolve(this.vscodeExtensionsFolder, path.basename(shortName + '.vsix')); - const finish = () => { - pluginResolverContext.addPlugin(originId, extensionPath); - resolve(); - }; - - const dest = fs.createWriteStream(extensionPath); - dest.addListener('finish', finish); - - request.get(asset.source) - .on('error', err => { - reject(err); - }).pipe(dest); - } else { - reject(new Error('Invalid status code' + response.statusCode + ' and message is ' + response.statusMessage)); - } - }); - }); - - } - - /** - * Handle only the plugins that starts with vscode: - */ - accept(pluginId: string): boolean { - return pluginId.startsWith(VsCodePluginDeployerResolver.PREFIX_VSCODE_EXTENSION) || - pluginId.startsWith(VsCodePluginDeployerResolver.PREFIX_EXT_INSTALL); - } -} diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts index a71ffafe33a65..06edb22316391 100644 --- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts +++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts @@ -43,7 +43,10 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca }, entryPoint: { backend: plugin.main - } + }, + iconUrl: plugin.icon && PluginPackage.toPluginUrl(plugin, plugin.icon), + readmeUrl: PluginPackage.toPluginUrl(plugin, './README.md'), + licenseUrl: PluginPackage.toPluginUrl(plugin, './LICENSE') }; return result; } @@ -65,7 +68,7 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca } } // Return the map of dependencies if present, else `undefined`. - return dependencies.size > 0 ? dependencies : undefined ; + return dependencies.size > 0 ? dependencies : undefined; } getLifecycle(plugin: PluginPackage): PluginLifecycle { diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index 688aa324f796a..037ba58df731c 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -30,6 +30,7 @@ "connect": "^3.7.0", "decompress": "^4.2.0", "escape-html": "^1.0.3", + "filenamify": "^4.1.0", "jsonc-parser": "^2.0.2", "lodash.clonedeep": "^4.5.0", "macaddress": "^0.2.9", diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 376a19abf0398..a43af8403a4e4 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -55,6 +55,7 @@ export interface PluginPackage { activationEvents?: string[]; extensionDependencies?: string[]; extensionPack?: string[]; + icon?: string; } export namespace PluginPackage { export function toPluginUrl(pck: PluginPackage, relativePath: string): string { @@ -267,8 +268,6 @@ export interface PluginScanner { getDependencies(plugin: PluginPackage): Map | undefined; } -export const PluginDeployer = Symbol('PluginDeployer'); - /** * A plugin resolver is handling how to resolve a plugin link into a local resource. */ @@ -315,12 +314,26 @@ export interface PluginDeployerResolverContext { } +export interface PluginDeployerStartContext { + readonly userEntries: string[] + readonly systemEntries: string[] +} + +export const PluginDeployer = Symbol('PluginDeployer'); export interface PluginDeployer { start(): void; } +export const PluginDeployerParticipant = Symbol('PluginDeployerParticipant'); +/** + * A participant can hook into the plugin deployer lifecycle. + */ +export interface PluginDeployerParticipant { + onWillStart?(context: PluginDeployerStartContext): Promise; +} + export enum PluginDeployerEntryType { FRONTEND, @@ -328,6 +341,14 @@ export enum PluginDeployerEntryType { BACKEND } +/** + * Whether a plugin installed by a user or system. + */ +export enum PluginType { + System, + User +}; + export interface PluginDeployerEntry { /** @@ -341,7 +362,7 @@ export interface PluginDeployerEntry { originalPath(): string; /** - * Local path on the filesystem + * Local path on the filesystem. */ path(): string; @@ -381,6 +402,15 @@ export interface PluginDeployerEntry { accept(...types: PluginDeployerEntryType[]): void; hasError(): boolean; + + type: PluginType + /** + * A fs path to a directory where a plugin is located. + * Depending on a plugin format it can be different from `path`. + * Use `path` if you want to resolve something within a plugin, like `README.md` file. + * Use `rootPath` if you want to manipulate the entire plugin location, like delete or move it. + */ + rootPath: string } export interface PluginDeployerFileHandlerContext { @@ -413,6 +443,9 @@ export interface PluginModel { }; entryPoint: PluginEntryPoint; packagePath: string; + iconUrl?: string; + readmeUrl?: string; + licenseUrl?: string; } export interface PluginEntryPoint { @@ -675,6 +708,8 @@ export interface PluginDeployerHandler { deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise; deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise; + undeployPlugin(pluginId: string): Promise; + getPluginDependencies(pluginToBeInstalled: PluginDeployerEntry): Promise } @@ -683,6 +718,10 @@ export interface GetDeployedPluginsParams { } export interface DeployedPlugin { + /** + * defaults to system + */ + type?: PluginType; metadata: PluginMetadata; contributes?: PluginContribution; } @@ -715,9 +754,13 @@ export const PluginServer = Symbol('PluginServer'); export interface PluginServer { /** - * Deploy a plugin + * Deploy a plugin. + * + * @param type whether a plugin is installed by a system or a user, defaults to a user */ - deploy(pluginEntry: string): Promise; + deploy(pluginEntry: string, type?: PluginType): Promise; + + undeploy(pluginId: string): Promise; setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise; getStorageValue(key: string, kind: PluginStorageKind): Promise; diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 645340e7fecbf..7fdc10311e29f 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -156,6 +156,22 @@ export class HostedPluginSupport { protected readonly onDidChangePluginsEmitter = new Emitter(); readonly onDidChangePlugins = this.onDidChangePluginsEmitter.event; + protected readonly deferredWillStart = new Deferred(); + /** + * Resolves when the initial plugins are loaded and about to be started. + */ + get willStart(): Promise { + return this.deferredWillStart.promise; + } + + protected readonly deferredDidStart = new Deferred(); + /** + * Resolves when the initial plugins are started. + */ + get didStart(): Promise { + return this.deferredDidStart.promise; + } + @postConstruct() protected init(): void { this.theiaReadyPromise = Promise.all([this.preferenceServiceImpl.ready, this.workspaceService.roots]); @@ -202,6 +218,11 @@ export class HostedPluginSupport { return plugins; } + getPlugin(id: string): DeployedPlugin | undefined { + const contributions = this.contributions.get(id); + return contributions && contributions.plugin; + } + /** do not call it, except from the plugin frontend contribution */ onStart(container: interfaces.Container): void { this.container = container; @@ -227,6 +248,10 @@ export class HostedPluginSupport { // process empty plugins as well in order to properly remove stale plugin widgets await this.syncPlugins(); + // it has to be resolved before awaiting layout is initilization + // otherwise clients can hang forever in the initializatin phase + this.deferredWillStart.resolve(); + // make sure that the previous state, including plugin widgets, is restored // and core layout is initialized, i.e. explorer, scm, debug views are already added to the shell // but shell is not yet revealed @@ -248,6 +273,9 @@ export class HostedPluginSupport { return; } await this.startPlugins(contributionsByHost, toDisconnect); + + this.deferredDidStart.resolve(); + this.restoreWebviews(); } diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts index 28f9726eaa59b..f8cd1a006389b 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts @@ -14,6 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import * as fs from 'fs-extra'; import { injectable, inject } from 'inversify'; import { ILogger } from '@theia/core'; import { PluginDeployerHandler, PluginDeployerEntry, PluginEntryPoint, DeployedPlugin, PluginDependencies } from '../../common/plugin-protocol'; @@ -29,6 +30,8 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { @inject(HostedPluginReader) private readonly reader: HostedPluginReader; + private readonly deployedLocations = new Map>(); + /** * Managed plugin metadata backend entries. */ @@ -113,17 +116,42 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { } const metadata = this.reader.readMetadata(manifest); - if (this.deployedBackendPlugins.has(metadata.model.id)) { + + const deployedLocations = this.deployedLocations.get(metadata.model.id) || new Set(); + deployedLocations.add(entry.rootPath); + this.deployedLocations.set(metadata.model.id, deployedLocations); + + const deployedPlugins = entryPoint === 'backend' ? this.deployedBackendPlugins : this.deployedFrontendPlugins; + if (deployedPlugins.has(metadata.model.id)) { return; } - const deployed: DeployedPlugin = { metadata }; + const { type } = entry; + const deployed: DeployedPlugin = { metadata, type }; deployed.contributes = this.reader.readContribution(manifest); - this.deployedBackendPlugins.set(metadata.model.id, deployed); + deployedPlugins.set(metadata.model.id, deployed); this.logger.info(`Deploying ${entryPoint} plugin "${metadata.model.name}@${metadata.model.version}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`); } catch (e) { console.error(`Failed to deploy ${entryPoint} plugin from '${pluginPath}' path`, e); } } + async undeployPlugin(pluginId: string): Promise { + this.deployedBackendPlugins.delete(pluginId); + this.deployedFrontendPlugins.delete(pluginId); + const deployedLocations = this.deployedLocations.get(pluginId); + if (!deployedLocations) { + return false; + } + this.deployedLocations.delete(pluginId); + for (const location of deployedLocations) { + try { + await fs.remove(location); + } catch (e) { + console.error(`[${pluginId}]: failed to undeploy from "${location}", reason`, e); + } + } + return true; + } + } diff --git a/packages/plugin-ext/src/main/common/plugin-theia-environment.ts b/packages/plugin-ext/src/main/common/plugin-theia-environment.ts new file mode 100644 index 0000000000000..c4958ea6bce4b --- /dev/null +++ b/packages/plugin-ext/src/main/common/plugin-theia-environment.ts @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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 PluginTheiaEnvironment { + + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; + + protected _pluginsDirUri: URI | undefined; + async getPluginsDirUri(): Promise { + if (!this._pluginsDirUri) { + const configDir = new URI(await this.environments.getConfigDirUri()); + this._pluginsDirUri = configDir.resolve('plugins'); + } + return this._pluginsDirUri; + } + +} diff --git a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts index c69e4049aa814..78f7fe1552270 100644 --- a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts +++ b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts @@ -14,30 +14,45 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext } from '../../../common/plugin-protocol'; -import { injectable } from 'inversify'; +import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandlerContext, PluginType } from '../../../common/plugin-protocol'; +import { injectable, inject } from 'inversify'; import { getTempDir } from '../temp-dir-util'; -import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as filenamify from 'filenamify'; +import { FileUri } from '@theia/core/lib/node/file-uri'; +import { PluginTheiaEnvironment } from '../../common/plugin-theia-environment'; @injectable() export class PluginTheiaFileHandler implements PluginDeployerFileHandler { - private unpackedFolder: string; - constructor() { - this.unpackedFolder = getTempDir('theia-unpacked'); - } + private readonly systemPluginsDirUri = FileUri.create(getTempDir('theia-unpacked')); + + @inject(PluginTheiaEnvironment) + protected readonly environment: PluginTheiaEnvironment; accept(resolvedPlugin: PluginDeployerEntry): boolean { return resolvedPlugin.isFile() && resolvedPlugin.path() !== null && resolvedPlugin.path().endsWith('.theia'); } async handle(context: PluginDeployerFileHandlerContext): Promise { - const unpackedPath = path.resolve(this.unpackedFolder, path.basename(context.pluginEntry().path())); - console.log(`unzipping the plug-in '${path.basename(context.pluginEntry().path())}' to directory: ${unpackedPath}`); - - await context.unzip(context.pluginEntry().path(), unpackedPath); + const id = context.pluginEntry().id(); + const pluginDir = await this.getPluginDir(context); + console.log(`[${id}]: trying to decompress into "${pluginDir}"...`); + if (context.pluginEntry().type === PluginType.User && await fs.pathExists(pluginDir)) { + console.log(`[${id}]: already found`); + context.pluginEntry().updatePath(pluginDir); + return; + } + await context.unzip(context.pluginEntry().path(), pluginDir); + console.log(`[${id}]: decompressed`); + context.pluginEntry().updatePath(pluginDir); + } - context.pluginEntry().updatePath(unpackedPath); - return Promise.resolve(); + protected async getPluginDir(context: PluginDeployerFileHandlerContext): Promise { + let pluginsDirUri = this.systemPluginsDirUri; + if (context.pluginEntry().type === PluginType.User) { + pluginsDirUri = await this.environment.getPluginsDirUri(); + } + return FileUri.fsPath(pluginsDirUri.resolve(filenamify(context.pluginEntry().id()))); } } diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts index 746acc3931a64..96477a9805e50 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-entry-impl.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { PluginDeployerEntry, PluginDeployerEntryType } from '../../common/plugin-protocol'; +import { PluginDeployerEntry, PluginDeployerEntryType, PluginType } from '../../common/plugin-protocol'; import * as fs from 'fs'; export class PluginDeployerEntryImpl implements PluginDeployerEntry { @@ -33,6 +33,9 @@ export class PluginDeployerEntryImpl implements PluginDeployerEntry { private resolvedByName: string; + private _type = PluginType.System; + private _rootPath: string | undefined; + constructor(readonly originId: string, readonly pluginId: string, initPath?: string) { this.map = new Map(); this.changes = []; @@ -108,4 +111,20 @@ export class PluginDeployerEntryImpl implements PluginDeployerEntry { return this.resolvedByName; } + get type(): PluginType { + return this._type; + } + + set type(type: PluginType) { + this._type = type; + } + + get rootPath(): string { + return !!this._rootPath ? this._rootPath : this.path(); + } + + set rootPath(rootPath: string) { + this._rootPath = rootPath; + } + } diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts index 4af261255851d..8b1d5a89a3781 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts @@ -16,11 +16,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { injectable, optional, multiInject, inject } from 'inversify'; +import { injectable, optional, multiInject, inject, named } from 'inversify'; import { PluginDeployerResolver, PluginDeployerFileHandler, PluginDeployerDirectoryHandler, - PluginDeployerEntry, PluginDeployer, PluginDeployerResolverInit, PluginDeployerFileHandlerContext, - PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginDeployerHandler + PluginDeployerEntry, PluginDeployer, PluginDeployerParticipant, PluginDeployerStartContext, + PluginDeployerResolverInit, PluginDeployerFileHandlerContext, + PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginDeployerHandler, PluginType } from '../../common/plugin-protocol'; import { PluginDeployerEntryImpl } from './plugin-deployer-entry-impl'; import { @@ -30,7 +31,7 @@ import { import { ProxyPluginDeployerEntry } from './plugin-deployer-proxy-entry-impl'; import { PluginDeployerFileHandlerContextImpl } from './plugin-deployer-file-handler-context-impl'; import { PluginDeployerDirectoryHandlerContextImpl } from './plugin-deployer-directory-handler-context-impl'; -import { ILogger, Emitter } from '@theia/core'; +import { ILogger, Emitter, ContributionProvider } from '@theia/core'; import { PluginCliContribution } from './plugin-cli-contribution'; import { performance } from 'perf_hooks'; @@ -67,6 +68,9 @@ export class PluginDeployerImpl implements PluginDeployer { @optional() @multiInject(PluginDeployerDirectoryHandler) private pluginDeployerDirectoryHandlers: PluginDeployerDirectoryHandler[]; + @inject(ContributionProvider) @named(PluginDeployerParticipant) + protected readonly participants: ContributionProvider; + public start(): void { this.logger.debug('Starting the deployer with the list of resolvers', this.pluginResolvers); this.doStart(); @@ -102,26 +106,62 @@ export class PluginDeployerImpl implements PluginDeployer { // transform it to array const defaultPluginIdList = defaultPluginsValue ? defaultPluginsValue.split(',') : []; const pluginIdList = pluginsValue ? pluginsValue.split(',') : []; - const pluginsList = defaultPluginIdList.concat(pluginIdList).concat(defaultPluginsValueViaCli ? defaultPluginsValueViaCli.split(',') : []); + const systemEntries = defaultPluginIdList.concat(pluginIdList).concat(defaultPluginsValueViaCli ? defaultPluginsValueViaCli.split(',') : []); + + const userEntries: string[] = []; + const context: PluginDeployerStartContext = { userEntries, systemEntries }; + + for (const contribution of this.participants.getContributions()) { + if (contribution.onWillStart) { + await contribution.onWillStart(context); + } + } const startDeployTime = performance.now(); - await this.deployMultipleEntries(pluginsList); + const [userPlugins, systemPlugins] = await Promise.all([ + this.resolvePlugins(context.userEntries, PluginType.User), + this.resolvePlugins(context.systemEntries, PluginType.System) + ]); + await this.deployPlugins([...userPlugins, ...systemPlugins]); this.logMeasurement('Deploy plugins list', startDeployTime); } - public async deploy(pluginEntry: string): Promise { + async undeploy(pluginId: string): Promise { + if (await this.pluginDeployerHandler.undeployPlugin(pluginId)) { + this.onDidDeployEmitter.fire(); + } + } + + async deploy(pluginEntry: string, type: PluginType = PluginType.System): Promise { const startDeployTime = performance.now(); - await this.deployMultipleEntries([pluginEntry]); + await this.deployMultipleEntries([pluginEntry], type); this.logMeasurement('Deploy plugin entry', startDeployTime); } - protected async deployMultipleEntries(pluginEntries: ReadonlyArray): Promise { + protected async deployMultipleEntries(pluginEntries: ReadonlyArray, type: PluginType = PluginType.System): Promise { + const pluginsToDeploy = await this.resolvePlugins(pluginEntries, type); + await this.deployPlugins(pluginsToDeploy); + } + + /** + * Resolves plugins for the given type. + * + * One can call it multiple times for different types before triggering a single deploy, i.e. + * ```ts + * const deployer: PluginDeployer; + * deployer.deployPlugins([ + * ...await deployer.resolvePlugins(userEntries, PluginType.User), + * ...await deployer.resolvePlugins(systemEntries, PluginType.System) + * ]); + * ``` + */ + async resolvePlugins(pluginEntries: ReadonlyArray, type: PluginType): Promise { const visited = new Set(); const pluginsToDeploy = new Map(); let queue = [...pluginEntries]; while (queue.length) { - const dependenciesChunk: Array< Map> = []; + const dependenciesChunk: Array> = []; const workload: string[] = []; while (queue.length) { const current = queue.shift()!; @@ -135,7 +175,7 @@ export class PluginDeployerImpl implements PluginDeployer { queue = []; await Promise.all(workload.map(async current => { try { - const pluginDeployerEntries = await this.resolvePlugin(current); + const pluginDeployerEntries = await this.resolvePlugin(current, type); await this.applyFileHandlers(pluginDeployerEntries); await this.applyDirectoryFileHandlers(pluginDeployerEntries); for (const deployerEntry of pluginDeployerEntries) { @@ -159,8 +199,7 @@ export class PluginDeployerImpl implements PluginDeployer { } } } - - await this.deployPlugins([...pluginsToDeploy.values()]); + return [...pluginsToDeploy.values()]; } /** @@ -234,7 +273,7 @@ export class PluginDeployerImpl implements PluginDeployer { /** * Check a plugin ID see if there are some resolvers that can handle it. If there is a matching resolver, then we resolve the plugin */ - public async resolvePlugin(pluginId: string): Promise { + public async resolvePlugin(pluginId: string, type: PluginType = PluginType.System): Promise { const pluginDeployerEntries: PluginDeployerEntry[] = []; const foundPluginResolver = this.pluginResolvers.find(pluginResolver => pluginResolver.accept(pluginId)); // there is a resolver for the input @@ -245,11 +284,16 @@ export class PluginDeployerImpl implements PluginDeployer { await foundPluginResolver.resolve(context); - context.getPlugins().forEach(entry => pluginDeployerEntries.push(entry)); + context.getPlugins().forEach(entry => { + entry.type = type; + pluginDeployerEntries.push(entry); + }); } else { // log it for now this.logger.error('No plugin resolver found for the entry', pluginId); - pluginDeployerEntries.push(new PluginDeployerEntryImpl(pluginId, pluginId)); + const unresolvedEntry = new PluginDeployerEntryImpl(pluginId, pluginId); + unresolvedEntry.type = type; + pluginDeployerEntries.push(unresolvedEntry); } return pluginDeployerEntries; diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts index 07041447e3ecb..f244d3d13b38d 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-proxy-entry-impl.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { injectable } from 'inversify'; -import { PluginDeployerEntry, PluginDeployerEntryType } from '../../common/plugin-protocol'; +import { PluginDeployerEntry, PluginDeployerEntryType, PluginType } from '../../common/plugin-protocol'; import { PluginDeployerEntryImpl } from './plugin-deployer-entry-impl'; /** @@ -77,4 +77,20 @@ export class ProxyPluginDeployerEntry implements PluginDeployerEntry { return this.delegate.resolvedBy(); } + get type(): PluginType { + return this.delegate.type; + } + + set type(type: PluginType) { + this.delegate.type = type; + } + + get rootPath(): string { + return this.delegate.rootPath; + } + + set rootPath(rootPath: string) { + this.delegate.rootPath = rootPath; + } + } diff --git a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts index c364c7ae25c89..418ac7e209fce 100644 --- a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts +++ b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts @@ -21,7 +21,7 @@ import { PluginsKeyValueStorage } from './plugins-key-value-storage'; import { PluginDeployerContribution } from './plugin-deployer-contribution'; import { PluginDeployer, PluginDeployerResolver, PluginDeployerFileHandler, - PluginDeployerDirectoryHandler, PluginServer, pluginServerJsonRpcPath + PluginDeployerDirectoryHandler, PluginServer, pluginServerJsonRpcPath, PluginDeployerParticipant } from '../../common/plugin-protocol'; import { PluginDeployerImpl } from './plugin-deployer-impl'; import { LocalDirectoryPluginDeployerResolver } from './resolvers/plugin-local-dir-resolver'; @@ -29,13 +29,15 @@ import { PluginTheiaFileHandler } from './handlers/plugin-theia-file-handler'; import { PluginTheiaDirectoryHandler } from './handlers/plugin-theia-directory-handler'; import { GithubPluginDeployerResolver } from './plugin-github-resolver'; import { HttpPluginDeployerResolver } from './plugin-http-resolver'; -import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core'; +import { ConnectionHandler, JsonRpcConnectionHandler, bindContributionProvider } from '@theia/core'; import { PluginPathsService, pluginPathsServicePath } from '../common/plugin-paths-protocol'; import { PluginPathsServiceImpl } from './paths/plugin-paths-service'; import { PluginServerHandler } from './plugin-server-handler'; import { PluginCliContribution } from './plugin-cli-contribution'; import { WebviewResourceLoaderImpl } from './webview-resource-loader-impl'; import { WebviewResourceLoaderPath } from '../common/webview-protocol'; +import { PluginTheiaEnvironment } from '../common/plugin-theia-environment'; +import { PluginTheiaDeployerParticipant } from './plugin-theia-deployer-participant'; export function bindMainBackend(bind: interfaces.Bind): void { bind(WebviewResourceLoaderImpl).toSelf().inSingletonScope(); @@ -48,6 +50,7 @@ export function bindMainBackend(bind: interfaces.Bind): void { bind(PluginApiContribution).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(PluginApiContribution); + bindContributionProvider(bind, PluginDeployerParticipant); bind(PluginDeployer).to(PluginDeployerImpl).inSingletonScope(); bind(PluginDeployerContribution).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(PluginDeployerContribution); @@ -56,6 +59,10 @@ export function bindMainBackend(bind: interfaces.Bind): void { bind(PluginDeployerResolver).to(GithubPluginDeployerResolver).inSingletonScope(); bind(PluginDeployerResolver).to(HttpPluginDeployerResolver).inSingletonScope(); + bind(PluginTheiaEnvironment).toSelf().inSingletonScope(); + bind(PluginTheiaDeployerParticipant).toSelf().inSingletonScope(); + bind(PluginDeployerParticipant).toService(PluginTheiaDeployerParticipant); + bind(PluginDeployerFileHandler).to(PluginTheiaFileHandler).inSingletonScope(); bind(PluginDeployerDirectoryHandler).to(PluginTheiaDirectoryHandler).inSingletonScope(); diff --git a/packages/plugin-ext/src/main/node/plugin-server-handler.ts b/packages/plugin-ext/src/main/node/plugin-server-handler.ts index 0251579fdccee..46dcb21ad2cc2 100644 --- a/packages/plugin-ext/src/main/node/plugin-server-handler.ts +++ b/packages/plugin-ext/src/main/node/plugin-server-handler.ts @@ -15,9 +15,10 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; +import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { PluginDeployerImpl } from './plugin-deployer-impl'; import { PluginsKeyValueStorage } from './plugins-key-value-storage'; -import { PluginServer, PluginDeployer, PluginStorageKind } from '../../common/plugin-protocol'; +import { PluginServer, PluginDeployer, PluginStorageKind, PluginType } from '../../common/plugin-protocol'; import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types'; @injectable() @@ -29,8 +30,16 @@ export class PluginServerHandler implements PluginServer { @inject(PluginsKeyValueStorage) protected readonly pluginsKeyValueStorage: PluginsKeyValueStorage; - deploy(pluginEntry: string): Promise { - return this.pluginDeployer.deploy(pluginEntry); + deploy(pluginEntry: string, arg2?: PluginType | CancellationToken): Promise { + const type = typeof arg2 === 'number' ? arg2 as PluginType : undefined; + return this.doDeploy(pluginEntry, type); + } + protected doDeploy(pluginEntry: string, type: PluginType = PluginType.User): Promise { + return this.pluginDeployer.deploy(pluginEntry, type); + } + + undeploy(pluginId: string): Promise { + return this.pluginDeployer.undeploy(pluginId); } setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise { diff --git a/packages/plugin-ext/src/main/node/plugin-theia-deployer-participant.ts b/packages/plugin-ext/src/main/node/plugin-theia-deployer-participant.ts new file mode 100644 index 0000000000000..a2b39cfe62d1a --- /dev/null +++ b/packages/plugin-ext/src/main/node/plugin-theia-deployer-participant.ts @@ -0,0 +1,32 @@ +/******************************************************************************** + * 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 { PluginTheiaEnvironment } from '../common/plugin-theia-environment'; +import { PluginDeployerParticipant, PluginDeployerStartContext } from '../../common/plugin-protocol'; + +@injectable() +export class PluginTheiaDeployerParticipant implements PluginDeployerParticipant { + + @inject(PluginTheiaEnvironment) + protected readonly environments: PluginTheiaEnvironment; + + async onWillStart(context: PluginDeployerStartContext): Promise { + const pluginsDirUri = await this.environments.getPluginsDirUri(); + context.userEntries.push(pluginsDirUri.withScheme('local-dir').toString()); + } + +} 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 883a4616e1da4..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" @@ -5677,6 +5765,15 @@ filename-reserved-regex@^2.0.0: resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" integrity sha1-q/c9+rc10EVECr/qLZHzieu/oik= +filenamify@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-4.1.0.tgz#54d110810ae74eebfe115c1b995bd07e03cf2184" + integrity sha512-KQV/uJDI9VQgN7sHH1Zbk6+42cD6mnQ2HONzkXUfPJ+K2FC8GZ1dpewbbHw0Sz8Tf5k3EVdHVayM4DoAwWlmtg== + dependencies: + filename-reserved-regex "^2.0.0" + strip-outer "^1.0.1" + trim-repeated "^1.0.0" + fill-range@^2.1.0: version "2.2.4" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" @@ -6613,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" @@ -7984,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" @@ -7999,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" @@ -8009,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" @@ -9979,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" @@ -11027,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" @@ -11210,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" @@ -11477,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" @@ -11752,6 +11916,13 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== +strip-outer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + strong-log-transformer@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-1.0.6.tgz#f7fb93758a69a571140181277eea0c2eb1301fa3" @@ -11811,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" @@ -12185,6 +12363,13 @@ trim-off-newlines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE= + dependencies: + escape-string-regexp "^1.0.2" + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -13151,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== @@ -13206,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" @@ -13271,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"