Skip to content

Commit

Permalink
Finish release channels UI
Browse files Browse the repository at this point in the history
Added a context menu entry for installed extensions to switch release
channels. This updates the privateExtensions.channels setting and asks
whether you want to update to the latest version on the new channel.

Replaced the channel label on the extension details page with a button
to switch channels. This only appears if there are multiple channels to
choose from.
  • Loading branch information
joelspadin-garmin committed Feb 12, 2020
1 parent ad869af commit 952fc77
Show file tree
Hide file tree
Showing 16 changed files with 433 additions and 74 deletions.
12 changes: 9 additions & 3 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,18 @@ You can use the **Private Extensions: Add Registry...** and
### Custom Channels

It is possible to create tracking channels by using npm dist-tags when
publishing a private extension. The tracked channel is used to determine when
extension updates are available.
publishing a private extension. This lets you publish pre-release or other
special versions of an extension without updating all users to them. Only users
who are tracking the specific release channel will get the updates.

#### Tracking a Channel

To specify a channel to track for an extension, add it to the `privateExtensions.channels` settings object. This is a dictionary where each key is an extension identifier
To switch release channels for an extension, install the extension, then
right-click it in the extensions list and select **Switch Release Channels...**.
Alternatively, click the **Channel** button on the extension details page.

You can manually select channels with the `privateExtensions.channels` settings
object. This is a dictionary where each key is an extension identifier
(`"${publisher}.${name}"`) and each name is the dist-tag to track, as shown in
the example below:

Expand Down
13 changes: 13 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@
"command": "privateExtensions.extension.install.anotherVersion",
"title": "%command.extension.install.anotherVersion.title%"
},
{
"command": "privateExtensions.extension.switchChannels",
"title": "%command.extension.switchChannels.title%"
},
{
"command": "privateExtensions.extension.copyInformation",
"title": "%command.extension.copyInformation.title%"
Expand Down Expand Up @@ -207,6 +211,11 @@
"when": "view =~ /^privateExtensions/ && viewItem =~ /installed|update/",
"group": "installVersion"
},
{
"command": "privateExtensions.extension.switchChannels",
"when": "view =~ /^privateExtensions/ && viewItem =~ /installed|update/",
"group": "installVersion"
},
{
"command": "privateExtensions.extension.copyInformation",
"when": "view =~ /^privateExtensions/ && viewItem =~ /extension/",
Expand Down Expand Up @@ -239,6 +248,10 @@
"command": "privateExtensions.extension.install.anotherVersion",
"when": "false"
},
{
"command": "privateExtensions.extension.switchChannels",
"when": "false"
},
{
"command": "privateExtensions.extension.copyInformation",
"when": "false"
Expand Down
1 change: 1 addition & 0 deletions extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"command.extension.update.title": "Update",
"command.extension.install.anotherVersion.title": "Install Another Version...",
"command.extension.copyInformation.title": "Copy Extension Information",
"command.extension.switchChannels.title": "Switch Release Channels...",
"command.registry.add.title": "Add User Registry...",
"command.registry.remove.title": "Remove User Registry",
"viewsContainers.activitybar.privateExtensions.title": "Private Extensions",
Expand Down
34 changes: 25 additions & 9 deletions extension/src/Package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';

import { getExtension } from './extensionInfo';
import { Registry } from './Registry';
import { Registry, VersionInfo } from './Registry';
import { LATEST } from './releaseChannel';
import { assertType, options } from './typeUtil';
import { isNonEmptyArray } from './util';
import { isNonEmptyArray, formatExtensionId } from './util';

const README_GLOB = 'README?(.*)';
const CHANGELOG_GLOB = 'CHANGELOG?(.*)';
Expand All @@ -24,6 +25,8 @@ export enum PackageState {
Installed = 'installed',
/** The latest version of the extension is already installed in the remote machine. */
InstalledRemote = 'installed.remote',
/** The latest version of the extension is installed from a pre-release channel. */
InstalledPrerelease = 'installed.prerelease',
/** The extension is installed and a newer version is available. */
UpdateAvailable = 'update',
/** The package is not a valid extension. */
Expand Down Expand Up @@ -108,7 +111,7 @@ export class Package {
* @param channel The NPM dist-tag this package is tracking, or a specific version it is pinned to.
* @throws {NotAnExtensionError} `manifest` is not a Visual Studio Code extension.
*/
constructor(registry: Registry, manifest: Record<string, unknown>, channel = 'latest') {
constructor(registry: Registry, manifest: Record<string, unknown>, channel = LATEST) {
this.registry = registry;

assertType(manifest, PackageManifest);
Expand All @@ -128,7 +131,7 @@ export class Package {
// Match that behavior by normalizing everything to lowercase.
this.isPublisherValid = !!manifest.publisher;
this.publisher = manifest.publisher ?? localize('publisher.unknown', 'Unknown');
this.extensionId = `${this.publisher}.${this.name}`.toLowerCase();
this.extensionId = formatExtensionId(this.publisher, this.name);

this.description = manifest.description ?? this.name;
this.version = parseVersion(manifest.version) ?? new SemVer('0.0.0');
Expand Down Expand Up @@ -167,14 +170,20 @@ export class Package {
if (this.isPublisherValid && this.vsixFile) {
if (this.isUpdateAvailable) {
return PackageState.UpdateAvailable;
} else if (this.isInstalled) {
}

if (this.isInstalled) {
if (this.channel !== LATEST) {
return PackageState.InstalledPrerelease;
}

return this.isUiExtension ? PackageState.Installed : PackageState.InstalledRemote;
} else {
return PackageState.Available;
}
} else {
return PackageState.Invalid;

return PackageState.Available;
}

return PackageState.Invalid;
}

/**
Expand Down Expand Up @@ -258,6 +267,13 @@ export class Package {
changelog: await findFile(directory, CHANGELOG_GLOB),
};
}

/**
* Gets the release channels available for the package.
*/
public getChannels(): Promise<Record<string, VersionInfo>> {
return this.registry.getPackageChannels(this.name);
}
}

function uriJoin(directory: vscode.Uri, file: string) {
Expand Down
49 changes: 34 additions & 15 deletions extension/src/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { CancellationToken, Uri, window } from 'vscode';
import * as nls from 'vscode-nls';

import { NotAnExtensionError, Package } from './Package';
import { getReleaseChannel, LATEST } from './releaseChannel';
import { assertType, options } from './typeUtil';
import { getConfig, getNpmCacheDir, getNpmDownloadDir, uriEquals } from './util';
import { getNpmCacheDir, getNpmDownloadDir, uriEquals } from './util';

const localize = nls.loadMessageBundle();

Expand Down Expand Up @@ -201,18 +202,36 @@ export class Registry {
return await npmfetch.json(`/${spec.escapedName}`, this.options);
}

/**
* Gets the release channels available for a package.
*
* This is a dictionary with channel names as keys and the latest version
* in each channel as values.
*/
public async getPackageChannels(name: string): Promise<Record<string, VersionInfo>> {
const metadata = await this.getPackageMetadata(name);

if (PackageVersionData.is(metadata)) {
const results: Record<string, VersionInfo> = {};

for (const key in metadata['dist-tags']) {
results[key] = getVersionInfo(metadata, metadata['dist-tags'][key]);
}

return results;
} else {
return {};
}
}

/**
* Gets the list of available versions for a package.
*/
public async getPackageVersions(name: string): Promise<VersionInfo[]> {
const metadata = await this.getPackageMetadata(name);

if (PackageVersionData.is(metadata)) {
return Object.keys(metadata.versions).map(key => {
const version = new SemVer(key);
const time = getVersionTimestamp(metadata, key);
return { version, time };
});
return Object.keys(metadata.versions).map(key => getVersionInfo(metadata, key));
} else {
return [];
}
Expand All @@ -234,11 +253,11 @@ export class Registry {
// Try to get publisher from latest release and use that to
// check for user-specified tracking channel.
if (version === undefined) {
const latest = lookupVersion(metadata, name, 'latest');
const latest = lookupVersion(metadata, name, LATEST);
if (typeof latest.publisher === 'string') {
version = getReleaseChannel(latest.publisher, name);
} else {
version = 'latest';
version = LATEST;
}
}

Expand Down Expand Up @@ -289,6 +308,13 @@ function getVersionTimestamp(meta: PackageVersionData, key: string) {
return time ? new Date(time) : undefined;
}

function getVersionInfo(metadata: PackageVersionData, version: string): VersionInfo {
return {
version: new SemVer(version),
time: getVersionTimestamp(metadata, version),
};
}

/**
Finds the version-specific metadata for a package given a version
or dist-tag.
Expand All @@ -306,11 +332,4 @@ function lookupVersion(metadata: PackageVersionData, name: string, versionOrTag:
return result;
}

/**
* Gets the user's selected release channel for an extension, or 'latest'.
*/
function getReleaseChannel(publisher: string, name: string) {
const id = `${publisher}.${name}`.toLowerCase();

return getConfig().get<Record<string, string>>('channels')?.[id] ?? 'latest';
}
Loading

0 comments on commit 952fc77

Please sign in to comment.