Skip to content

Commit 627dad4

Browse files
authoredFeb 28, 2024··
Introduce '--install-plugin' cli command. (#13421)
Fixes #13406 Inspired by the VS Code --install-extension command. Users can give the option multiple times with either a plugin id of the form "publisher.name[@Version]" or with a file path designating a *.vsix file. contributed on behalf of STMicroelectronics Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
1 parent d22e3e2 commit 627dad4

12 files changed

+140
-49
lines changed
 

‎CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## v1.47.0 not yet released
88

9-
- [component] add here
9+
- [plugin] Add command to install plugins from the command line [#13406](https://github.com/eclipse-theia/theia/issues/13406) - contributed on behalf of STMicroelectronics
1010

1111
<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a>
1212
- [monaco] Upgrade Monaco dependency to 1.83.1 [#13217](https://github.com/eclipse-theia/theia/pull/13217)- contributed on behalf of STMicroelectronics\

‎packages/core/src/common/promise-util.spec.ts

+48-18
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616
import * as assert from 'assert/strict';
17-
import { firstTrue, waitForEvent } from './promise-util';
17+
import { Deferred, firstTrue, waitForEvent } from './promise-util';
1818
import { Emitter } from './event';
1919
import { CancellationError } from './cancellation';
2020

@@ -35,37 +35,67 @@ describe('promise-util', () => {
3535
});
3636
});
3737

38+
type ExecutionHandler<T> = (resolve: (value: T) => void, reject: (error: unknown) => void) => void;
39+
3840
describe('firstTrue', () => {
41+
function createSequentialPromises<T>(...executionHandlers: ExecutionHandler<T>[]): Promise<T>[] {
42+
const deferreds: Deferred<T>[] = [];
43+
let i = 0;
44+
for (let k = 0; k < executionHandlers.length; k++) {
45+
deferreds.push(new Deferred<T>());
46+
}
47+
48+
const resolveNext = () => {
49+
if (i < executionHandlers.length) {
50+
executionHandlers[i](value => deferreds[i].resolve(value), error => deferreds[i].reject(error));
51+
i++;
52+
}
53+
if (i < executionHandlers.length) {
54+
setTimeout(resolveNext, 1);
55+
}
56+
};
57+
58+
setTimeout(resolveNext, 1);
59+
return deferreds.map(deferred => deferred.promise);
60+
}
61+
3962
it('should resolve to false when the promises arg is empty', async () => {
4063
const actual = await firstTrue();
4164
assert.strictEqual(actual, false);
4265
});
4366

4467
it('should resolve to true when the first promise resolves to true', async () => {
4568
const signals: string[] = [];
46-
const createPromise = (signal: string, timeout: number, result: boolean) =>
47-
new Promise<boolean>(resolve => setTimeout(() => {
69+
70+
function createHandler(signal: string, result?: boolean): ExecutionHandler<boolean> {
71+
return (resolve: (value: boolean) => void, reject: (error: unknown) => void) => {
4872
signals.push(signal);
49-
resolve(result);
50-
}, timeout));
51-
const actual = await firstTrue(
52-
createPromise('a', 10, false),
53-
createPromise('b', 20, false),
54-
createPromise('c', 30, true),
55-
createPromise('d', 40, false),
56-
createPromise('e', 50, true)
57-
);
73+
if (typeof result !== 'undefined') {
74+
resolve(result);
75+
} else {
76+
reject(undefined);
77+
}
78+
};
79+
}
80+
81+
const actual = await firstTrue(...createSequentialPromises(
82+
createHandler('a', false),
83+
createHandler('b', false),
84+
createHandler('c', true),
85+
createHandler('d', false),
86+
createHandler('e', true)
87+
));
5888
assert.strictEqual(actual, true);
5989
assert.deepStrictEqual(signals, ['a', 'b', 'c']);
6090
});
6191

6292
it('should reject when one of the promises rejects', async () => {
63-
await assert.rejects(firstTrue(
64-
new Promise<boolean>(resolve => setTimeout(() => resolve(false), 10)),
65-
new Promise<boolean>(resolve => setTimeout(() => resolve(false), 20)),
66-
new Promise<boolean>((_, reject) => setTimeout(() => reject(new Error('my test error')), 30)),
67-
new Promise<boolean>(resolve => setTimeout(() => resolve(true), 40)),
68-
), /Error: my test error/);
93+
await assert.rejects(firstTrue(...createSequentialPromises<boolean>(
94+
(resolve, _) => resolve(false),
95+
resolve => resolve(false),
96+
(_, reject) => reject(new Error('my test error')),
97+
resolve => resolve(true),
98+
)), /Error: my test error/);
6999
});
70100
});
71101

‎packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ export class PluginVscodeCommandsContribution implements CommandContribution {
356356
commands.registerCommand({ id: VscodeCommands.INSTALL_FROM_VSIX.id }, {
357357
execute: async (vsixUriOrExtensionId: TheiaURI | UriComponents | string) => {
358358
if (typeof vsixUriOrExtensionId === 'string') {
359-
await this.pluginServer.deploy(VSCodeExtensionUri.toVsxExtensionUriString(vsixUriOrExtensionId));
359+
await this.pluginServer.deploy(VSCodeExtensionUri.fromId(vsixUriOrExtensionId).toString());
360360
} else {
361361
const uriPath = isUriComponents(vsixUriOrExtensionId) ? URI.revive(vsixUriOrExtensionId).fsPath : await this.fileService.fsPath(vsixUriOrExtensionId);
362362
await this.pluginServer.deploy(`local-file:${uriPath}`);

‎packages/plugin-ext-vscode/src/common/plugin-vscode-uri.ts

+15-17
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,24 @@ import URI from '@theia/core/lib/common/uri';
2121
* In practice, this means that it will be resolved and deployed by the Open-VSX system.
2222
*/
2323
export namespace VSCodeExtensionUri {
24-
export const VSCODE_PREFIX = 'vscode:extension/';
25-
/**
26-
* Should be used to prefix a plugin's ID to ensure that it is identified as a VSX Extension.
27-
* @returns `vscode:extension/${id}`
28-
*/
29-
export function toVsxExtensionUriString(id: string): string {
30-
return `${VSCODE_PREFIX}${id}`;
31-
}
32-
export function toUri(name: string, namespace: string): URI;
33-
export function toUri(id: string): URI;
34-
export function toUri(idOrName: string, namespace?: string): URI {
35-
if (typeof namespace === 'string') {
36-
return new URI(toVsxExtensionUriString(`${namespace}.${idOrName}`));
24+
export const SCHEME = 'vscode-extension';
25+
26+
export function fromId(id: string, version?: string): URI {
27+
if (typeof version === 'string') {
28+
return new URI().withScheme(VSCodeExtensionUri.SCHEME).withAuthority(id).withPath(`/${version}`);
3729
} else {
38-
return new URI(toVsxExtensionUriString(idOrName));
30+
return new URI().withScheme(VSCodeExtensionUri.SCHEME).withAuthority(id);
3931
}
4032
}
41-
export function toId(uri: URI): string | undefined {
42-
if (uri.scheme === 'vscode' && uri.path.dir.toString() === 'extension') {
43-
return uri.path.base;
33+
34+
export function fromVersionedId(versionedId: string): URI {
35+
const versionAndId = versionedId.split('@');
36+
return fromId(versionAndId[0], versionAndId[1]);
37+
}
38+
39+
export function toId(uri: URI): { id: string, version?: string } | undefined {
40+
if (uri.scheme === VSCodeExtensionUri.SCHEME) {
41+
return { id: uri.authority, version: uri.path.isRoot ? undefined : uri.path.base };
4442
}
4543
return undefined;
4644
}

‎packages/plugin-ext-vscode/src/node/scanner-vscode.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca
9191
// Iterate over the list of dependencies present, and add them to the collection.
9292
dependency.forEach((dep: string) => {
9393
const dependencyId = dep.toLowerCase();
94-
dependencies.set(dependencyId, VSCodeExtensionUri.toVsxExtensionUriString(dependencyId));
94+
dependencies.set(dependencyId, VSCodeExtensionUri.fromId(dependencyId).toString());
9595
});
9696
}
9797
}

‎packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class VSXExtensionEditorManager extends WidgetOpenHandler<VSXExtensionEdi
3636
if (!id) {
3737
throw new Error('Invalid URI: ' + uri.toString());
3838
}
39-
return { id };
39+
return id;
4040
}
4141

4242
}

‎packages/vsx-registry/src/browser/vsx-extension.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
146146
}
147147

148148
get uri(): URI {
149-
return VSCodeExtensionUri.toUri(this.id);
149+
return VSCodeExtensionUri.fromId(this.id);
150150
}
151151

152152
get id(): string {

‎packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export class VSXLanguageQuickPickService extends LanguageQuickPickService {
7272
localizationContribution.localizedLanguageName ?? localizationContribution.languageName ?? localizationContribution.languageId),
7373
});
7474
try {
75-
const extensionUri = VSCodeExtensionUri.toUri(extension.extension.name, extension.extension.namespace).toString();
75+
const extensionUri = VSCodeExtensionUri.fromId(extension.extension.name, extension.extension.namespace).toString();
7676
await this.pluginServer.deploy(extensionUri);
7777
} finally {
7878
progress.cancel();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 TypeFox and others.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { inject, injectable } from '@theia/core/shared/inversify';
18+
import { PluginDeployerParticipant, PluginDeployerStartContext } from '@theia/plugin-ext';
19+
import { VsxCli } from './vsx-cli';
20+
import { VSXExtensionUri } from '../common';
21+
import * as fs from 'fs';
22+
import { FileUri } from '@theia/core/lib/node';
23+
import * as path from 'path';
24+
25+
@injectable()
26+
export class VsxCliDeployerParticipant implements PluginDeployerParticipant {
27+
28+
@inject(VsxCli)
29+
protected readonly vsxCli: VsxCli;
30+
31+
async onWillStart(context: PluginDeployerStartContext): Promise<void> {
32+
const pluginUris = this.vsxCli.pluginsToInstall.map(async id => {
33+
try {
34+
const resolvedPath = path.resolve(id);
35+
const stat = await fs.promises.stat(resolvedPath);
36+
if (stat.isFile()) {
37+
return FileUri.create(resolvedPath).withScheme('local-file').toString();
38+
}
39+
} catch (e) {
40+
// expected if file does not exist
41+
}
42+
return VSXExtensionUri.fromVersionedId(id).toString();
43+
});
44+
context.userEntries.push(...await Promise.all(pluginUris));
45+
}
46+
}

‎packages/vsx-registry/src/node/vsx-cli.ts

+13
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,28 @@ import * as fs from 'fs';
2424
export class VsxCli implements CliContribution {
2525

2626
ovsxRouterConfig: OVSXRouterConfig | undefined;
27+
pluginsToInstall: string[] = [];
2728

2829
configure(conf: Argv<{}>): void {
2930
conf.option('ovsx-router-config', { description: 'JSON configuration file for the OVSX router client', type: 'string' });
31+
conf.option('install-plugin', {
32+
alias: 'install-extension',
33+
nargs: 1,
34+
desc: 'Installs or updates a plugin. Argument is a path to the *.vsix file or a plugin id of the form "publisher.name[@version]"'
35+
});
3036
}
3137

3238
async setArguments(args: Record<string, unknown>): Promise<void> {
3339
const { 'ovsx-router-config': ovsxRouterConfig } = args;
3440
if (typeof ovsxRouterConfig === 'string') {
3541
this.ovsxRouterConfig = JSON.parse(await fs.promises.readFile(ovsxRouterConfig, 'utf8'));
3642
}
43+
let pluginsToInstall = args.installPlugin;
44+
if (typeof pluginsToInstall === 'string') {
45+
pluginsToInstall = [pluginsToInstall];
46+
}
47+
if (Array.isArray(pluginsToInstall)) {
48+
this.pluginsToInstall = pluginsToInstall;
49+
}
3750
}
3851
}

‎packages/vsx-registry/src/node/vsx-extension-resolver.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ export class VSXExtensionResolver implements PluginDeployerResolver {
5151
}
5252
let extension: VSXExtensionRaw | undefined;
5353
const client = await this.clientProvider();
54-
if (options) {
55-
console.log(`[${id}]: trying to resolve version ${options.version}...`);
56-
const { extensions } = await client.query({ extensionId: id, extensionVersion: options.version, includeAllVersions: true });
54+
const version = options?.version || id.version;
55+
if (version) {
56+
console.log(`[${id}]: trying to resolve version ${version}...`);
57+
const { extensions } = await client.query({ extensionId: id.id, extensionVersion: version, includeAllVersions: true });
5758
extension = extensions[0];
5859
} else {
5960
console.log(`[${id}]: trying to resolve latest version...`);
60-
const { extensions } = await client.query({ extensionId: id, includeAllVersions: true });
61+
const { extensions } = await client.query({ extensionId: id.id, includeAllVersions: true });
6162
extension = this.vsxApiFilter.getLatestCompatibleExtension(extensions);
6263
}
6364
if (!extension) {
@@ -66,12 +67,12 @@ export class VSXExtensionResolver implements PluginDeployerResolver {
6667
if (extension.error) {
6768
throw new Error(extension.error);
6869
}
69-
const resolvedId = id + '-' + extension.version;
70+
const resolvedId = id.id + '-' + extension.version;
7071
const downloadUrl = extension.files.download;
71-
console.log(`[${id}]: resolved to '${resolvedId}'`);
72+
console.log(`[${id.id}]: resolved to '${resolvedId}'`);
7273

7374
if (!options?.ignoreOtherVersions) {
74-
const existingVersion = this.hasSameOrNewerVersion(id, extension);
75+
const existingVersion = this.hasSameOrNewerVersion(id.id, extension);
7576
if (existingVersion) {
7677
console.log(`[${id}]: is already installed with the same or newer version '${existingVersion}'`);
7778
return;

‎packages/vsx-registry/src/node/vsx-registry-backend-module.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
1818
import { CliContribution } from '@theia/core/lib/node';
1919
import { ContainerModule } from '@theia/core/shared/inversify';
20-
import { PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol';
20+
import { PluginDeployerParticipant, PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol';
2121
import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment';
2222
import { VsxCli } from './vsx-cli';
2323
import { VSXEnvironmentImpl } from './vsx-environment-impl';
2424
import { VSXExtensionResolver } from './vsx-extension-resolver';
25+
import { VsxCliDeployerParticipant } from './vsx-cli-deployer-participant';
2526

2627
export default new ContainerModule(bind => {
2728
bind(VSXEnvironment).to(VSXEnvironmentImpl).inSingletonScope();
@@ -32,4 +33,6 @@ export default new ContainerModule(bind => {
3233
.inSingletonScope();
3334
bind(VSXExtensionResolver).toSelf().inSingletonScope();
3435
bind(PluginDeployerResolver).toService(VSXExtensionResolver);
36+
bind(VsxCliDeployerParticipant).toSelf().inSingletonScope();
37+
bind(PluginDeployerParticipant).toService(VsxCliDeployerParticipant);
3538
});

0 commit comments

Comments
 (0)
Please sign in to comment.