Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement downloader #2

Merged
merged 16 commits into from
Nov 5, 2023
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
"command": "biome.restartLspServer",
"title": "Restart LSP Server",
"category": "Biome"
},
{
"command": "biome.clearVersionsCache",
"title": "Clear versions cache",
"category": "Biome"
}
],
"menus": {
Expand Down Expand Up @@ -149,13 +154,16 @@
"@biomejs/biome": "^1.2.2",
"@types/node": "^18.17.5",
"@types/resolve": "^1.20.2",
"@types/semver": "^7.5.4",
"@types/vscode": "^1.80.0",
"@vscode/vsce": "^2.20.1",
"esbuild": "^0.19.2",
"typescript": "^5.1.6"
},
"dependencies": {
"resolve": "^1.22.4",
"semver": "^7.5.4",
"undici": "^5.27.2",
"vscode-languageclient": "^8.1.0"
},
"vsce": {
Expand Down
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// list of commands available in the VS Code extension
export enum Commands {
StopServer = "biome.stopServer",
RestartLspServer = "biome.restartLspServer",
SyntaxTree = "biome.syntaxTree",
ServerStatus = "biome.serverStatus",
UpdateBiome = "biome.updateBiome",
ChangeVersion = "biome.changeVersion",
}
186 changes: 186 additions & 0 deletions src/downloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { chmodSync } from "node:fs";
import { coerce, rcompare } from "semver";
import { fetch } from "undici";
import {
ExtensionContext,
ProgressLocation,
Uri,
commands,
window,
workspace,
} from "vscode";
import { Commands } from "./commands";

export const selectAndDownload = async (
context: ExtensionContext,
): Promise<string | undefined> => {
const versions = await window.withProgress(
{
location: ProgressLocation.Notification,
title: "Fetching Biome versions",
cancellable: false,
},
async () => {
return await getVersions(context);
},
);

const version = await askVersion(versions);

if (!version) {
return undefined;
}

return await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Downloading Biome ${version}`,
cancellable: false,
},
async () => {
await commands.executeCommand(Commands.StopServer);
await download(version, context);
await commands.executeCommand(Commands.RestartLspServer);
return version;
},
);
};

export const updateToLatest = async (context: ExtensionContext) => {
await window.withProgress(
{
location: ProgressLocation.Notification,
title: "Updating Biome version",
cancellable: false,
},
async () => {
const versions = await getVersions(context);
const version = versions[0];
await commands.executeCommand(Commands.StopServer);
await download(version, context);
await commands.executeCommand(Commands.RestartLspServer);
},
);
};

/**
* Download the Biome CLI from GitHub
*
* @param version The version to download
*/
const download = async (version: string, context: ExtensionContext) => {
const releases = (await (
await fetch(
`https://api.github.com/repos/biomejs/biome/releases/tags/cli/v${version}`,
)
).json()) as {
assets: { name: string; browser_download_url: string }[];
};

const platformArch = `${process.platform}-${process.arch}`;

// Find the asset for the current platform
const asset = releases.assets.find(
(asset) =>
asset.name ===
`biome-${platformArch}${process.platform === "win32" ? ".exe" : ""}`,
);

if (!asset) {
window.showErrorMessage(
`The specified version is not available for your platform/architecture (${platformArch}).`,
);
return;
}
nhedger marked this conversation as resolved.
Show resolved Hide resolved

let bin: ArrayBuffer;
try {
const blob = await fetch(asset.browser_download_url);
bin = await blob.arrayBuffer();
} catch {
window.showErrorMessage(
`Could not download the binary for your platform/architecture (${platformArch}).`,
);
return;
}

// Write binary file to disk
await workspace.fs.writeFile(
Uri.joinPath(
context.globalStorageUri,
"server",
`biome${process.platform === "win32" ? ".exe" : ""}`,
),
new Uint8Array(bin),
nhedger marked this conversation as resolved.
Show resolved Hide resolved
);

// Make biome executable
chmodSync(
Uri.joinPath(
context.globalStorageUri,
"server",
`biome${process.platform === "win32" ? ".exe" : ""}`,
).fsPath,
0o755,
);

// Record latest version
await context.globalState.update("bundled_biome_version", version);
};

/**
* Display the VS Code prompt for selection the version
*/
const askVersion = async (versions: string[]): Promise<string | undefined> => {
const options = versions.map((version, index) => ({
label: version,
description: index === 0 ? "(latest)" : "",
}));

const result = await window.showQuickPick(options, {
placeHolder: "Select the version of the biome CLI to install",
});

return result?.label;
};

/**
* Retrieves the list of versions of the CLI.
*
* The calls to the API are cached for 1 hour to prevent hitting the rate limit.
*/
export const getVersions = async (
context: ExtensionContext,
): Promise<string[]> => {
const cachedVersions = context.globalState.get<{
expires_at: Date;
versions: string[];
}>("biome_versions_cache");

// If the cache exists and is still valid, return it
if (cachedVersions && new Date(cachedVersions.expires_at) > new Date()) {
return cachedVersions.versions;
}

const releases = (await (
await fetch(
"https://api.github.com/repos/biomejs/biome/releases?per_page=100",
)
).json()) as { tag_name: string }[];

const versions = releases
.filter((release) => release.tag_name.startsWith("cli/"))
.map((release) => release.tag_name.replace("cli/", ""))
.map((release) => coerce(release))
.sort((a, b) => rcompare(a, b))
.filter((release) => release?.version !== null)
.map((release) => release?.version);

// Cache the result for 1 hour
await context.globalState.update("biome_versions_cache", {
expires_at: new Date(Date.now() + 60 * 60 * 1000),
versions,
});
ematipico marked this conversation as resolved.
Show resolved Hide resolved

return versions;
};
Loading