7
7
8
8
import * as vscode from 'vscode'
9
9
import { existsSync } from 'fs'
10
+ import * as semver from 'semver'
10
11
import { join } from 'path'
11
12
import { getLogger } from './logger/logger'
12
13
import { telemetry } from './telemetry'
13
14
import { VSCODE_EXTENSION_ID } from './extensions'
14
- import { extensionVersion } from './vscode/env'
15
15
16
16
/**
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 .
19
19
*
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
25
24
*/
26
25
async 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 >
31
31
) : 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
+
38
34
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 ) ) ,
42
38
] )
43
39
44
- const installedExtensionsCount = extensionsDirContent
45
- . map ( ( [ name ] ) => name )
46
- . filter ( ( name ) => name . includes ( extension ) ) . length
47
-
48
40
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
+ }
57
59
}
58
60
} catch ( error ) {
59
61
getLogger ( ) . error ( `UninstallExtension: Failed to check .obsolete: ${ error } ` )
60
62
}
61
63
}
62
64
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
+
63
109
/**
64
110
* Sets up a file system watcher to monitor the .obsolete file for changes and handle
65
111
* extension un-installation if the extension is marked as obsolete.
@@ -71,6 +117,7 @@ async function checkExtensionUninstall(
71
117
*/
72
118
export function setupUninstallHandler (
73
119
extensionName : typeof VSCODE_EXTENSION_ID . awstoolkit | typeof VSCODE_EXTENSION_ID . amazonq ,
120
+ extensionVersion : string ,
74
121
context : vscode . ExtensionContext ,
75
122
callback : ( ) => Promise < void > = async ( ) => { }
76
123
) : void {
@@ -86,7 +133,7 @@ export function setupUninstallHandler(
86
133
const fileWatcher = vscode . workspace . createFileSystemWatcher ( watchPattern )
87
134
88
135
const checkUninstallHandler = ( ) =>
89
- checkExtensionUninstall ( extensionName , extensionsDirPath , obsoleteFilePath , callback )
136
+ checkExtensionUninstall ( extensionName , extensionVersion , extensionsDirPath , obsoleteFilePath , callback )
90
137
fileWatcher . onDidCreate ( checkUninstallHandler )
91
138
fileWatcher . onDidChange ( checkUninstallHandler )
92
139
0 commit comments