From 863c0cc87187e7577427a904d209a31b392f34f4 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Sat, 30 Dec 2023 16:21:38 +0000 Subject: [PATCH] Overhaul extension bundling (#844) * Overhaul extension bundling * Check version matches requested + stylua update * Prompt do not show again on updates * Add logging + check if binary found on PATH is executable * Update changelog --- stylua-vscode/CHANGELOG.md | 6 ++ stylua-vscode/package-lock.json | 25 +++++ stylua-vscode/package.json | 7 ++ stylua-vscode/src/download.ts | 186 ++++++++++++++++++++++---------- stylua-vscode/src/extension.ts | 63 +++++++++-- 5 files changed, 218 insertions(+), 69 deletions(-) diff --git a/stylua-vscode/CHANGELOG.md b/stylua-vscode/CHANGELOG.md index 1b69274a..90fa382f 100644 --- a/stylua-vscode/CHANGELOG.md +++ b/stylua-vscode/CHANGELOG.md @@ -17,6 +17,9 @@ To view the changelog of the StyLua binary, see [here](https://github.com/Johnny ### Added +- The extension now supports using a StyLua binary found on the PATH + - This can be configured via setting `stylua.searchBinaryOnPATH` + - If the binary fails to execute, we fall back to the bundled version - Added configuration option `stylua.configPath` to provide a direct path to a `stylua.toml` file. Note: this will override any workspace config lookup - Added configuration option `stylua.verify` to pass `--verify` to StyLua CLI when formatting a file. This enforces output verification - Added command `StyLua: Select Version` to customize which version of StyLua to install. This command updates the `stylua.targetReleaseVersion` setting @@ -27,6 +30,9 @@ To view the changelog of the StyLua binary, see [here](https://github.com/Johnny - Removed excessive error notifications on formatting failure and replaced with VSCode language status bar item - `.styluaignore` is now registered as an ignore file with an appropriate file icon +- StyLua version updates will now be shown on the status bar. To disable these notifications, configure `stylua.disableVersionCheck` +- If `stylua.targetReleaseVersion` is set, we will now still notify about the latest release version +- If `stylua.targetReleaseVersion` is set, but the installed version does not match, prompt to install desired version ## [1.5.0] - 2023-03-11 diff --git a/stylua-vscode/package-lock.json b/stylua-vscode/package-lock.json index 062bf59a..f7645b10 100644 --- a/stylua-vscode/package-lock.json +++ b/stylua-vscode/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "ignore": "^5.1.8", "node-fetch": "^2.6.1", + "node-which": "^1.0.0", "semver": "^7.5.4", "unzipper": "^0.10.14" }, @@ -21,6 +22,7 @@ "@types/node-fetch": "^2.5.8", "@types/unzipper": "^0.10.8", "@types/vscode": "^1.82.0", + "@types/which": "^3.0.3", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "@vscode/test-cli": "^0.0.4", @@ -348,6 +350,12 @@ "integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==", "dev": true }, + "node_modules/@types/which": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz", + "integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz", @@ -2128,6 +2136,12 @@ } } }, + "node_modules/node-which": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-which/-/node-which-1.0.0.tgz", + "integrity": "sha512-PVVbv6DyDc3VkTizcd/7Xchn3NDh+OUnQkrHxgumQ+2lwoWE36KuQClfAVjTwEh3T0jzb1WHrbgCol5HbBZqdQ==", + "deprecated": "please use 'which'" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3149,6 +3163,12 @@ "integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==", "dev": true }, + "@types/which": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz", + "integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz", @@ -4452,6 +4472,11 @@ "whatwg-url": "^5.0.0" } }, + "node-which": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-which/-/node-which-1.0.0.tgz", + "integrity": "sha512-PVVbv6DyDc3VkTizcd/7Xchn3NDh+OUnQkrHxgumQ+2lwoWE36KuQClfAVjTwEh3T0jzb1WHrbgCol5HbBZqdQ==" + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/stylua-vscode/package.json b/stylua-vscode/package.json index 77e949e4..1a7fb8ea 100644 --- a/stylua-vscode/package.json +++ b/stylua-vscode/package.json @@ -103,6 +103,11 @@ "default": null, "description": "Specifies the path of StyLua. If not specified, will automatically download one from the GitHub releases." }, + "stylua.searchBinaryInPATH": { + "type": "boolean", + "default": true, + "markdownDescription": "Search for the StyLua binary in the `PATH` environment variable, and use this if available. If disabled, falls back to a bundled binary" + }, "stylua.disableVersionCheck": { "type": "boolean", "default": false, @@ -143,6 +148,7 @@ "@types/node-fetch": "^2.5.8", "@types/unzipper": "^0.10.8", "@types/vscode": "^1.82.0", + "@types/which": "^3.0.3", "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "@vscode/test-cli": "^0.0.4", @@ -156,6 +162,7 @@ "dependencies": { "ignore": "^5.1.8", "node-fetch": "^2.6.1", + "node-which": "^1.0.0", "semver": "^7.5.4", "unzipper": "^0.10.14" } diff --git a/stylua-vscode/src/download.ts b/stylua-vscode/src/download.ts index 0e80d061..794b710f 100644 --- a/stylua-vscode/src/download.ts +++ b/stylua-vscode/src/download.ts @@ -1,14 +1,23 @@ import * as vscode from "vscode"; import * as unzip from "unzipper"; import * as util from "./util"; +import * as semver from "semver"; import fetch from "node-fetch"; import { createWriteStream } from "fs"; import { executeStylua } from "./stylua"; import { GitHub, GitHubRelease } from "./github"; +import which = require("which"); -interface StyluaInfo { +export enum ResolveMode { + configuration = "configuration", + path = "PATH", + bundled = "bundled", +} + +export interface StyluaInfo { path: string; - version: string | undefined; + resolveMode: ResolveMode; + version?: string | undefined; } const getStyluaVersion = async (path: string, cwd?: string) => { @@ -22,52 +31,100 @@ const getStyluaVersion = async (path: string, cwd?: string) => { } }; -export class StyluaDownloader { +export class StyluaDownloader implements vscode.Disposable { + statusBarUpdateItem = vscode.window.createStatusBarItem( + "stylua.installUpdate", + vscode.StatusBarAlignment.Right + ); + constructor( private readonly storageDirectory: vscode.Uri, - private readonly github: GitHub + private readonly github: GitHub, + private readonly outputChannel: vscode.LogOutputChannel ) {} + public async findStylua(cwd?: string): Promise { + // 1) If `stylua.styluaPath` has been specified, use that directly + const settingPath = vscode.workspace + .getConfiguration("stylua") + .get("styluaPath"); + if (settingPath) { + this.outputChannel.info( + `Stylua path explicitly configured: ${settingPath}` + ); + return { path: settingPath, resolveMode: ResolveMode.configuration }; + } + + // 2) Find a `stylua` binary available on PATH + if ( + vscode.workspace + .getConfiguration("stylua") + .get("searchBinaryInPATH") + ) { + this.outputChannel.info("Searching for stylua on PATH"); + const resolvedPath = await which("stylua", { nothrow: true }); + if (resolvedPath) { + this.outputChannel.info(`Stylua found on PATH: ${resolvedPath}`); + if (await getStyluaVersion(resolvedPath, cwd)) { + return { path: resolvedPath, resolveMode: ResolveMode.path }; + } else { + this.outputChannel.error( + "Stylua binary found on PATH failed to execute" + ); + } + } + } + + // 3) Fallback to bundled stylua version + this.outputChannel.info("Falling back to bundled StyLua version"); + const downloadPath = vscode.Uri.joinPath( + this.storageDirectory, + util.getDownloadOutputFilename() + ); + return { path: downloadPath.fsPath, resolveMode: ResolveMode.bundled }; + } + public async ensureStyluaExists( cwd?: string ): Promise { - const path = await this.getStyluaPath(); - - if (path === undefined) { - await vscode.workspace.fs.createDirectory(this.storageDirectory); - await this.downloadStyLuaVisual(util.getDesiredVersion()); - const path = await this.getStyluaPath(); - if (path) { - return { - path, - version: await getStyluaVersion(path), - }; - } else { - return; + const stylua = await this.findStylua(cwd); + + if (stylua.resolveMode === ResolveMode.bundled) { + if (!(await util.fileExists(stylua.path))) { + await vscode.workspace.fs.createDirectory(this.storageDirectory); + await this.downloadStyLuaVisual(util.getDesiredVersion()); } - } else { - if (!(await util.fileExists(path))) { - vscode.window.showErrorMessage( - `The path given for StyLua (${path}) does not exist` - ); - return; + stylua.version = await getStyluaVersion(stylua.path, cwd); + + // Check bundled version matches requested version + const desiredVersion = util.getDesiredVersion(); + if (stylua.version && desiredVersion !== "latest") { + const desiredVersionSemver = semver.coerce(desiredVersion); + const styluaVersionSemver = semver.parse(stylua.version); + if ( + desiredVersionSemver && + styluaVersionSemver && + semver.neq(desiredVersionSemver, styluaVersionSemver) + ) { + this.openIncorrectVersionPrompt(stylua.version, desiredVersion); + } } - const currentVersion = await getStyluaVersion(path); - + // Check for latest version if ( !vscode.workspace.getConfiguration("stylua").get("disableVersionCheck") ) { try { - const desiredVersion = util.getDesiredVersion(); - const release = await this.github.getRelease(desiredVersion); + const latestRelease = await this.github.getRelease("latest"); if ( - currentVersion !== - (release.tagName.startsWith("v") - ? release.tagName.substr(1) - : release.tagName) + stylua.version !== + (latestRelease.tagName.startsWith("v") + ? latestRelease.tagName.substr(1) + : latestRelease.tagName) ) { - this.openUpdatePrompt(release); + this.showUpdateAvailable(latestRelease); + } else { + this.statusBarUpdateItem.hide(); } } catch (err) { vscode.window.showWarningMessage( @@ -86,10 +143,22 @@ export class StyluaDownloader { } } } + } else { + this.statusBarUpdateItem.hide(); } - - return { path, version: currentVersion }; + } else if (stylua.resolveMode === ResolveMode.configuration) { + if (!(await util.fileExists(stylua.path))) { + vscode.window.showErrorMessage( + `The path given for StyLua (${stylua.path}) does not exist` + ); + return; + } + stylua.version = await getStyluaVersion(stylua.path, cwd); + } else if (stylua.resolveMode === ResolveMode.path) { + stylua.version = await getStyluaVersion(stylua.path, cwd); } + + return stylua; } public downloadStyLuaVisual(version: string): Thenable { @@ -140,41 +209,40 @@ export class StyluaDownloader { } } - private openUpdatePrompt(release: GitHubRelease) { + private showUpdateAvailable(release: GitHubRelease) { + this.statusBarUpdateItem.name = "StyLua Update"; + this.statusBarUpdateItem.text = `StyLua update available (${release.tagName}) $(cloud-download)`; + this.statusBarUpdateItem.tooltip = "Click to update StyLua"; + this.statusBarUpdateItem.command = { + title: "Update StyLua", + command: "stylua.installUpdate", + arguments: [release], + }; + this.statusBarUpdateItem.backgroundColor = new vscode.ThemeColor( + "statusBarItem.warningBackground" + ); + this.statusBarUpdateItem.show(); + } + + private openIncorrectVersionPrompt( + currentVersion: string, + requestedVersion: string + ) { vscode.window .showInformationMessage( - `StyLua ${release.tagName} is available to install.`, - "Install", - "Later", - "Release Notes" + `The currently installed version of StyLua (${currentVersion}) does not match the requested version (${requestedVersion})`, + "Install" ) .then((option) => { switch (option) { case "Install": - this.downloadStyLuaVisual(release.tagName); - break; - case "Release Notes": - vscode.env.openExternal(vscode.Uri.parse(release.htmlUrl)); - this.openUpdatePrompt(release); + vscode.commands.executeCommand("stylua.reinstall"); break; } }); } - public async getStyluaPath(): Promise { - const settingPath = vscode.workspace - .getConfiguration("stylua") - .get("styluaPath"); - if (settingPath) { - return settingPath; - } - - const downloadPath = vscode.Uri.joinPath( - this.storageDirectory, - util.getDownloadOutputFilename() - ); - if (await util.fileExists(downloadPath)) { - return downloadPath.fsPath; - } + dispose() { + this.statusBarUpdateItem.dispose(); } } diff --git a/stylua-vscode/src/extension.ts b/stylua-vscode/src/extension.ts index 0ce004d9..18e0ca19 100644 --- a/stylua-vscode/src/extension.ts +++ b/stylua-vscode/src/extension.ts @@ -1,8 +1,8 @@ import * as vscode from "vscode"; import * as semver from "semver"; import { formatCode, checkIgnored } from "./stylua"; -import { GitHub } from "./github"; -import { StyluaDownloader } from "./download"; +import { GitHub, GitHubRelease } from "./github"; +import { ResolveMode, StyluaDownloader, StyluaInfo } from "./download"; import { getDesiredVersion } from "./util"; const documentSelector = ["lua", "luau"]; @@ -27,7 +27,7 @@ const byteOffset = ( class StatusInfo implements vscode.Disposable { statusItem: vscode.LanguageStatusItem; - version: string | undefined; + styluaInfo: StyluaInfo | undefined; constructor() { this.statusItem = vscode.languages.createLanguageStatusItem( @@ -42,13 +42,20 @@ class StatusInfo implements vscode.Disposable { this.updateReady(); } - setVersion(version: string | undefined) { - this.version = version; + setStyluaInfo(styluaInfo: StyluaInfo | undefined) { + this.styluaInfo = styluaInfo; this.updateReady(); } getStyluaText() { - return this.version ? `StyLua (${this.version})` : "StyLua"; + if (this.styluaInfo && this.styluaInfo.version) { + if (this.styluaInfo.resolveMode === ResolveMode.bundled) { + return `StyLua (bundled ${this.styluaInfo.version})`; + } else { + return `StyLua (${this.styluaInfo.version})`; + } + } + return "StyLua"; } updateReady() { @@ -86,7 +93,11 @@ export async function activate(context: vscode.ExtensionContext) { const github = new GitHub(); context.subscriptions.push(github); - const downloader = new StyluaDownloader(context.globalStorageUri, github); + const downloader = new StyluaDownloader( + context.globalStorageUri, + github, + outputChannel + ); let cwdForVersionDetection = vscode.workspace.workspaceFolders?.[0].uri.fsPath; @@ -94,7 +105,7 @@ export async function activate(context: vscode.ExtensionContext) { let styluaBinaryPath = await downloader.ensureStyluaExists( cwdForVersionDetection ); - statusItem.setVersion(styluaBinaryPath?.version); + statusItem.setStyluaInfo(styluaBinaryPath); context.subscriptions.push( vscode.commands.registerCommand("stylua.reinstall", async () => { @@ -102,7 +113,7 @@ export async function activate(context: vscode.ExtensionContext) { styluaBinaryPath = await downloader.ensureStyluaExists( cwdForVersionDetection ); - statusItem.setVersion(styluaBinaryPath?.version); + statusItem.setStyluaInfo(styluaBinaryPath); }) ); @@ -159,13 +170,45 @@ export async function activate(context: vscode.ExtensionContext) { }) ); + context.subscriptions.push( + vscode.commands.registerCommand( + "stylua.installUpdate", + async (release: GitHubRelease) => { + const result = await vscode.window.showInformationMessage( + `Are you sure you want to update StyLua to ${release.tagName}?`, + { modal: true }, + "Update", + "Release Notes", + "Do not show again" + ); + + switch (result) { + case "Update": + await downloader.downloadStyLuaVisual(release.tagName); + vscode.workspace + .getConfiguration("stylua") + .update("targetReleaseVersion", "latest"); + break; + case "Release Notes": + vscode.env.openExternal(vscode.Uri.parse(release.htmlUrl)); + break; + case "Do not show again": + vscode.workspace + .getConfiguration("stylua") + .update("disableVersionCheck", true); + break; + } + } + ) + ); + context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (change) => { if (change.affectsConfiguration("stylua")) { styluaBinaryPath = await downloader.ensureStyluaExists( cwdForVersionDetection ); - statusItem.setVersion(styluaBinaryPath?.version); + statusItem.setStyluaInfo(styluaBinaryPath); } }) );