77
88import * as vscode from 'vscode'
99import { existsSync } from 'fs'
10+ import * as semver from 'semver'
1011import { join } from 'path'
1112import { getLogger } from './logger/logger'
1213import { telemetry } from './telemetry'
1314import { VSCODE_EXTENSION_ID } from './extensions'
14- import { extensionVersion } from './vscode/env'
1515
1616/**
17- * Checks if the extension has been uninstalled by reading the .obsolete file
18- * and comparing the number of obsolete extensions with the installed extensions .
17+ * Checks if an extension has been uninstalled and performs a callback if so.
18+ * This function differentiates between an uninstall and an auto-update .
1919 *
20- * @param {string } extensionName - The name of the extension.
21- * @param {string } extensionsDirPath - The path to the extensions directory.
22- * @param {string } obsoleteFilePath - The path to the .obsolete file.
23- * @param {function } callback - Action performed when extension is uninstalled.
24- * @returns {void }
20+ * @param extensionId - The ID of the extension to check (e.g., VSCODE_EXTENSION_ID.awstoolkit)
21+ * @param extensionsPath - The file system path to the VS Code extensions directory
22+ * @param obsoletePath - The file system path to the .obsolete file
23+ * @param onUninstallCallback - A callback function to execute if the extension is uninstalled
2524 */
2625async function checkExtensionUninstall (
27- extensionName : typeof VSCODE_EXTENSION_ID . awstoolkit | typeof VSCODE_EXTENSION_ID . amazonq ,
28- extensionsDirPath : string ,
29- obsoleteFilePath : string ,
30- callback : ( ) => Promise < void >
26+ extensionId : typeof VSCODE_EXTENSION_ID . awstoolkit | typeof VSCODE_EXTENSION_ID . amazonq ,
27+ extensionVersion : string ,
28+ extensionsPath : string ,
29+ obsoletePath : string ,
30+ onUninstallCallback : ( ) => Promise < void >
3131) : Promise < void > {
32- /**
33- * Users can have multiple profiles with different versions of the extensions.
34- *
35- * This makes sure the callback is triggered only when an explicit extension with specific version is uninstalled.
36- */
37- const extension = `${ extensionName } -${ extensionVersion } `
32+ const extensionFullName = `${ extensionId } -${ extensionVersion } `
33+
3834 try {
39- const [ obsoleteFileContent , extensionsDirContent ] = await Promise . all ( [
40- vscode . workspace . fs . readFile ( vscode . Uri . file ( obsoleteFilePath ) ) ,
41- vscode . workspace . fs . readDirectory ( vscode . Uri . file ( extensionsDirPath ) ) ,
35+ const [ obsoleteFileContent , extensionDirEntries ] = await Promise . all ( [
36+ vscode . workspace . fs . readFile ( vscode . Uri . file ( obsoletePath ) ) ,
37+ vscode . workspace . fs . readDirectory ( vscode . Uri . file ( extensionsPath ) ) ,
4238 ] )
4339
44- const installedExtensionsCount = extensionsDirContent
45- . map ( ( [ name ] ) => name )
46- . filter ( ( name ) => name . includes ( extension ) ) . length
47-
4840 const obsoleteExtensions = JSON . parse ( obsoleteFileContent . toString ( ) )
49- const obsoleteExtensionsCount = Object . keys ( obsoleteExtensions ) . filter ( ( id ) => id . includes ( extension ) ) . length
50-
51- if ( installedExtensionsCount === obsoleteExtensionsCount ) {
52- await callback ( )
53- telemetry . aws_extensionUninstalled . run ( ( span ) => {
54- span . record ( { } )
55- } )
56- getLogger ( ) . info ( `UninstallExtension: ${ extension } uninstalled successfully` )
41+ const currentExtension = vscode . extensions . getExtension ( extensionId )
42+
43+ if ( ! currentExtension ) {
44+ // Check if the extension was previously installed and is now in the obsolete list
45+ const wasObsolete = Object . keys ( obsoleteExtensions ) . some ( ( id ) => id . startsWith ( extensionId ) )
46+ if ( wasObsolete ) {
47+ await handleUninstall ( extensionFullName , onUninstallCallback )
48+ }
49+ } else {
50+ // Check if there's a newer version in the extensions directory
51+ const newerVersionExists = checkForNewerVersion ( extensionDirEntries , extensionId , extensionVersion )
52+
53+ if ( ! newerVersionExists ) {
54+ // No newer version exists, so this is likely an uninstall
55+ await handleUninstall ( extensionFullName , onUninstallCallback )
56+ } else {
57+ getLogger ( ) . info ( `UpdateExtension: ${ extensionFullName } is being updated - not an uninstall` )
58+ }
5759 }
5860 } catch ( error ) {
5961 getLogger ( ) . error ( `UninstallExtension: Failed to check .obsolete: ${ error } ` )
6062 }
6163}
6264
65+ /**
66+ * Checks if a newer version of the extension exists in the extensions directory.
67+ * The isExtensionInstalled fn is used to determine if the extension is installed using the vscode API
68+ * whereas this function checks for the newer version in the extension directory for scenarios where
69+ * the old extension is un-installed and the new extension in downloaded but not installed.
70+ *
71+ * @param dirEntries - The entries in the extensions directory
72+ * @param extensionId - The ID of the extension to check
73+ * @param currentVersion - The current version of the extension
74+ * @returns True if a newer version exists, false otherwise
75+ */
76+
77+ function checkForNewerVersion (
78+ dirEntries : [ string , vscode . FileType ] [ ] ,
79+ extensionId : string ,
80+ currentVersion : string
81+ ) : boolean {
82+ const versionRegex = new RegExp ( `^${ extensionId } -(.+)$` )
83+
84+ return dirEntries
85+ . map ( ( [ name ] ) => name )
86+ . filter ( ( name ) => name . startsWith ( extensionId ) )
87+ . some ( ( name ) => {
88+ const match = name . match ( versionRegex )
89+ if ( match && match [ 1 ] ) {
90+ const version = semver . valid ( semver . coerce ( match [ 1 ] ) )
91+ return version !== null && semver . gt ( version , currentVersion )
92+ }
93+ return false
94+ } )
95+ }
96+
97+ /**
98+ * Handles the uninstall process by calling the callback and logging the event.
99+ *
100+ * @param extensionFullName - The full name of the extension including version
101+ * @param callback - The callback function to execute on uninstall
102+ */
103+ async function handleUninstall ( extensionFullName : string , callback : ( ) => Promise < void > ) : Promise < void > {
104+ await callback ( )
105+ telemetry . aws_extensionUninstalled . run ( ( ) => { } )
106+ getLogger ( ) . info ( `UninstallExtension: ${ extensionFullName } uninstalled successfully` )
107+ }
108+
63109/**
64110 * Sets up a file system watcher to monitor the .obsolete file for changes and handle
65111 * extension un-installation if the extension is marked as obsolete.
@@ -71,6 +117,7 @@ async function checkExtensionUninstall(
71117 */
72118export function setupUninstallHandler (
73119 extensionName : typeof VSCODE_EXTENSION_ID . awstoolkit | typeof VSCODE_EXTENSION_ID . amazonq ,
120+ extensionVersion : string ,
74121 context : vscode . ExtensionContext ,
75122 callback : ( ) => Promise < void > = async ( ) => { }
76123) : void {
@@ -86,7 +133,7 @@ export function setupUninstallHandler(
86133 const fileWatcher = vscode . workspace . createFileSystemWatcher ( watchPattern )
87134
88135 const checkUninstallHandler = ( ) =>
89- checkExtensionUninstall ( extensionName , extensionsDirPath , obsoleteFilePath , callback )
136+ checkExtensionUninstall ( extensionName , extensionVersion , extensionsDirPath , obsoleteFilePath , callback )
90137 fileWatcher . onDidCreate ( checkUninstallHandler )
91138 fileWatcher . onDidChange ( checkUninstallHandler )
92139
0 commit comments