From 77d3de1fca535bd3049d8e811709e13721245888 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 25 Oct 2025 18:14:24 +0530 Subject: [PATCH 1/5] feat: deprecated extensions like emment installed we show a dialog once that its deprecated --- .../deprecated-extensions-dialog.html | 28 +++++++++ src/nls/root/strings.js | 4 ++ src/utils/ExtensionLoader.js | 61 ++++++++++++++++++- 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/htmlContent/deprecated-extensions-dialog.html diff --git a/src/htmlContent/deprecated-extensions-dialog.html b/src/htmlContent/deprecated-extensions-dialog.html new file mode 100644 index 0000000000..620ea88f23 --- /dev/null +++ b/src/htmlContent/deprecated-extensions-dialog.html @@ -0,0 +1,28 @@ + diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 63f9db31ed..4229ec7de1 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1723,6 +1723,10 @@ define({ "LICENSE_ACTIVATE_FAIL_APPLY": "'Failed to apply license to device'", "LICENSE_ENTER_KEY": "Please enter a license key", "LICENSE_REAPPLY_TO_DEVICE": "Already activated? Reapply system-wide", + // Deprecated Extensions Dialog + "DEPRECATED_EXTENSIONS_TITLE": "Deprecated Extensions Detected", + "DEPRECATED_EXTENSIONS_MESSAGE": "The following installed extensions are now natively supported by Phoenix and can be safely removed:", + "DEPRECATED_EXTENSIONS_LEARN_MORE": "Learn more about the native feature", // AI CONTROL "AI_LOGIN_DIALOG_TITLE": "Sign In to Use AI Edits", "AI_LOGIN_DIALOG_MESSAGE": "Please log in to use AI-powered edits", diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 35d793436c..8ed86bdd54 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -54,11 +54,19 @@ define(function (require, exports, module) { UrlParams = require("utils/UrlParams").UrlParams, NodeUtils = require("utils/NodeUtils"), PathUtils = require("thirdparty/path-utils/path-utils"), - DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json")); + DefaultExtensions = JSON.parse(require("text!extensions/default/DefaultExtensions.json")), + Dialogs = require("widgets/Dialogs"), + PreferencesManager = require("preferences/PreferencesManager"), + Mustache = require("thirdparty/mustache/mustache"), + Strings = require("strings"), + DeprecatedExtensionsTemplate = require("text!htmlContent/deprecated-extensions-dialog.html"); // takedown/dont load extensions that are compromised at app start - start const EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY = "PH_EXTENSION_TAKEDOWN_LIST"; + // deprecated extensions dialog state key + const STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN = "deprecatedExtensionsDialogShown"; + function _getTakedownListLS() { try{ let list = localStorage.getItem(EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY); @@ -1018,6 +1026,8 @@ define(function (require, exports, module) { promise.always(function () { _init = true; + // Check for deprecated extensions after all extensions have loaded + _checkAndShowDeprecatedExtensionsDialog(); }); return promise; @@ -1032,6 +1042,55 @@ define(function (require, exports, module) { return takedownExtensionList.has(extensionID); } + /** + * Check if any deprecated extensions are installed and show a dialog once + * @private + */ + function _checkAndShowDeprecatedExtensionsDialog() { + // Check if we've already shown the dialog + const dialogShown = PreferencesManager.stateManager.get(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN); + if (dialogShown) { + return; + } + + // Get deprecated extensions config + const deprecatedExtensionsConfig = DefaultExtensions.deprecatedExtensions; + if (!deprecatedExtensionsConfig || !deprecatedExtensionsConfig.extensionIDsAndDocs) { + return; + } + + const deprecatedExtensionIDs = deprecatedExtensionsConfig.extensionIDsAndDocs; + + // Check which deprecated extensions are loaded + const deprecatedExtensionsFound = []; + for (const extensionID of loadedExtensionIDs) { + if (deprecatedExtensionIDs[extensionID]) { + deprecatedExtensionsFound.push({ + id: extensionID, + docUrl: deprecatedExtensionIDs[extensionID] + }); + } + } + + // If no deprecated extensions found, mark dialog as shown and return + if (deprecatedExtensionsFound.length === 0) { + PreferencesManager.stateManager.set(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN, true); + return; + } + + // Show the dialog + const templateVars = { + extensions: deprecatedExtensionsFound, + Strings: Strings + }; + + const $template = $(Mustache.render(DeprecatedExtensionsTemplate, templateVars)); + Dialogs.showModalDialogUsingTemplate($template); + + // Mark dialog as shown + PreferencesManager.stateManager.set(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN, true); + } + EventDispatcher.makeEventDispatcher(exports); From 111eacc75a58b8e060f137723b8254aa19214cb5 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 25 Oct 2025 18:30:19 +0530 Subject: [PATCH 2/5] fix: deprecated extensions dialog wont come up again and styling tweaks --- .../deprecated-extensions-dialog.html | 2 +- src/nls/root/strings.js | 4 +-- src/utils/ExtensionLoader.js | 28 ++++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/htmlContent/deprecated-extensions-dialog.html b/src/htmlContent/deprecated-extensions-dialog.html index 620ea88f23..89eb2f00b5 100644 --- a/src/htmlContent/deprecated-extensions-dialog.html +++ b/src/htmlContent/deprecated-extensions-dialog.html @@ -7,7 +7,7 @@

{{Strings.DEPRECATED_EXTENSIONS_TITLE}}

{{#extensions}} -
+
{{id}} diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 4229ec7de1..05eeef04bc 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1725,8 +1725,8 @@ define({ "LICENSE_REAPPLY_TO_DEVICE": "Already activated? Reapply system-wide", // Deprecated Extensions Dialog "DEPRECATED_EXTENSIONS_TITLE": "Deprecated Extensions Detected", - "DEPRECATED_EXTENSIONS_MESSAGE": "The following installed extensions are now natively supported by Phoenix and can be safely removed:", - "DEPRECATED_EXTENSIONS_LEARN_MORE": "Learn more about the native feature", + "DEPRECATED_EXTENSIONS_MESSAGE": "The following installed extensions are now natively supported by {APP_NAME} and can be safely uninstalled from the Extension Manager:", + "DEPRECATED_EXTENSIONS_LEARN_MORE": "Now built-in — learn more", // AI CONTROL "AI_LOGIN_DIALOG_TITLE": "Sign In to Use AI Edits", "AI_LOGIN_DIALOG_MESSAGE": "Please log in to use AI-powered edits", diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 8ed86bdd54..7a41e22f07 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -1043,16 +1043,10 @@ define(function (require, exports, module) { } /** - * Check if any deprecated extensions are installed and show a dialog once + * Check if any deprecated extensions are installed and show a dialog once per extension * @private */ function _checkAndShowDeprecatedExtensionsDialog() { - // Check if we've already shown the dialog - const dialogShown = PreferencesManager.stateManager.get(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN); - if (dialogShown) { - return; - } - // Get deprecated extensions config const deprecatedExtensionsConfig = DefaultExtensions.deprecatedExtensions; if (!deprecatedExtensionsConfig || !deprecatedExtensionsConfig.extensionIDsAndDocs) { @@ -1061,10 +1055,16 @@ define(function (require, exports, module) { const deprecatedExtensionIDs = deprecatedExtensionsConfig.extensionIDsAndDocs; - // Check which deprecated extensions are loaded + // Get the state object that tracks which deprecated extensions we've already shown + let shownDeprecatedExtensions = PreferencesManager.stateManager.get(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN); + if (!shownDeprecatedExtensions || typeof shownDeprecatedExtensions !== 'object') { + shownDeprecatedExtensions = {}; + } + + // Check which deprecated extensions are loaded and not yet shown const deprecatedExtensionsFound = []; for (const extensionID of loadedExtensionIDs) { - if (deprecatedExtensionIDs[extensionID]) { + if (deprecatedExtensionIDs[extensionID] && !shownDeprecatedExtensions[extensionID]) { deprecatedExtensionsFound.push({ id: extensionID, docUrl: deprecatedExtensionIDs[extensionID] @@ -1072,9 +1072,8 @@ define(function (require, exports, module) { } } - // If no deprecated extensions found, mark dialog as shown and return + // If no new deprecated extensions found, return if (deprecatedExtensionsFound.length === 0) { - PreferencesManager.stateManager.set(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN, true); return; } @@ -1087,8 +1086,11 @@ define(function (require, exports, module) { const $template = $(Mustache.render(DeprecatedExtensionsTemplate, templateVars)); Dialogs.showModalDialogUsingTemplate($template); - // Mark dialog as shown - PreferencesManager.stateManager.set(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN, true); + // Mark each extension as shown + for (const ext of deprecatedExtensionsFound) { + shownDeprecatedExtensions[ext.id] = true; + } + PreferencesManager.stateManager.set(STATE_DEPRECATED_EXTENSIONS_DIALOG_SHOWN, shownDeprecatedExtensions); } From 62bc33107b25f7fc34eb65632550ade297ee5e96 Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 25 Oct 2025 18:49:45 +0530 Subject: [PATCH 3/5] feat: add uninstall extension button in deprecated extension dialog --- .../deprecated-extensions-dialog.html | 25 ++++++++++++------- src/utils/ExtensionLoader.js | 16 ++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/htmlContent/deprecated-extensions-dialog.html b/src/htmlContent/deprecated-extensions-dialog.html index 89eb2f00b5..2d1257464b 100644 --- a/src/htmlContent/deprecated-extensions-dialog.html +++ b/src/htmlContent/deprecated-extensions-dialog.html @@ -7,16 +7,23 @@

{{Strings.DEPRECATED_EXTENSIONS_TITLE}}

{{#extensions}} -
-
- - {{id}} +
+ - {{/extensions}} diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 7a41e22f07..199c906083 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -1042,6 +1042,15 @@ define(function (require, exports, module) { return takedownExtensionList.has(extensionID); } + /** + * Uninstall a deprecated extension + * @param {string} extensionID - The ID of the extension to uninstall + */ + function uninstallExtension(extensionID) { + // TODO: Implement uninstall logic + alert(`Uninstall button clicked for extension: ${extensionID}`); + } + /** * Check if any deprecated extensions are installed and show a dialog once per extension * @private @@ -1086,6 +1095,12 @@ define(function (require, exports, module) { const $template = $(Mustache.render(DeprecatedExtensionsTemplate, templateVars)); Dialogs.showModalDialogUsingTemplate($template); + // Wire up uninstall button click handlers + $template.on('click', '.uninstall-extension-btn', function() { + const extensionID = $(this).data('extension-id'); + uninstallExtension(extensionID); + }); + // Mark each extension as shown for (const ext of deprecatedExtensionsFound) { shownDeprecatedExtensions[ext.id] = true; @@ -1117,6 +1132,7 @@ define(function (require, exports, module) { exports.loadAllExtensionsInNativeDirectory = loadAllExtensionsInNativeDirectory; exports.loadExtensionFromNativeDirectory = loadExtensionFromNativeDirectory; exports.isExtensionTakenDown = isExtensionTakenDown; + exports.uninstallExtension = uninstallExtension; exports.testAllExtensionsInNativeDirectory = testAllExtensionsInNativeDirectory; exports.testAllDefaultExtensions = testAllDefaultExtensions; exports.EVENT_EXTENSION_LOADED = EVENT_EXTENSION_LOADED; From e3fd707dd88c7de0fc0e9ca5d808f91fe7cbcf5d Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 25 Oct 2025 19:34:14 +0530 Subject: [PATCH 4/5] feat: add support to uninstall deprected extensions as the dialog comes up itself --- .../deprecated-extensions-dialog.html | 2 +- src/nls/root/strings.js | 4 + src/styles/brackets_patterns_override.less | 4 + src/utils/ExtensionLoader.js | 90 ++++++++++++++++--- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/htmlContent/deprecated-extensions-dialog.html b/src/htmlContent/deprecated-extensions-dialog.html index 2d1257464b..eb6792f73e 100644 --- a/src/htmlContent/deprecated-extensions-dialog.html +++ b/src/htmlContent/deprecated-extensions-dialog.html @@ -11,7 +11,7 @@

{{Strings.DEPRECATED_EXTENSIONS_TITLE}}

- {{id}} + {{name}}
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 05eeef04bc..e6c14a3ada 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -804,6 +804,8 @@ define({ "EXTENSION_VERIFIED_SORT": "Verified", "EXTENSION_STAR": "Star", "EXTENSION_STAR_SORT": "Star Rating", + "ERROR_UNINSTALLING_EXTENSION_TITLE": "Error Uninstalling Extension", + "ERROR_UNINSTALLING_EXTENSION_MESSAGE": "Failed to uninstall extension {0}", // For NOT_FOUND_ERR, see generic strings above "EXTENSION_MANAGER_TITLE": "Extension Manager", "EXTENSION_MANAGER_ERROR_LOAD": "Unable to access the extension registry. Please try again later.", @@ -1171,10 +1173,12 @@ define({ "DOWNLOAD_ERROR": "Error occurred while downloading.", "NETWORK_SLOW_OR_DISCONNECTED": "Network is disconnected or too slow.", "RESTART_BUTTON": "Restart", + "RESTART_APP_BUTTON": "Restart {APP_NAME}", "LATER_BUTTON": "Later", "DESCRIPTION_AUTO_UPDATE": "Enable/disable {APP_NAME} Auto-update", "AUTOUPDATE_ERROR": "Error!", "AUTOUPDATE_IN_PROGRESS": "An update is already in progress.", + "REMOVING": "Removing\u2026", "NUMBER_WITH_PERCENTAGE": "{0}%", // Strings for Related Files diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 1c69fab684..b6359abeab 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -2619,3 +2619,7 @@ code { margin-top: 2px; } } + +.striked { + text-decoration: line-through; +} diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 199c906083..3d963570f8 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -59,7 +59,10 @@ define(function (require, exports, module) { PreferencesManager = require("preferences/PreferencesManager"), Mustache = require("thirdparty/mustache/mustache"), Strings = require("strings"), - DeprecatedExtensionsTemplate = require("text!htmlContent/deprecated-extensions-dialog.html"); + StringUtils = require("utils/StringUtils"), + Metrics = require("utils/Metrics"), + DeprecatedExtensionsTemplate = require("text!htmlContent/deprecated-extensions-dialog.html"), + CommandManager = require("command/CommandManager"); // takedown/dont load extensions that are compromised at app start - start const EXTENSION_TAKEDOWN_LOCALSTORAGE_KEY = "PH_EXTENSION_TAKEDOWN_LIST"; @@ -82,7 +85,7 @@ define(function (require, exports, module) { return []; } - const loadedExtensionIDs = new Set(); + const loadedExtensionIDs = new Map(); let takedownExtensionList = new Set(_getTakedownListLS()); const EXTENSION_TAKEDOWN_URL = brackets.config.extensionTakedownURL; @@ -96,16 +99,16 @@ define(function (require, exports, module) { if (takedownExtensionList.size < loadedExtensionIDs.size) { smaller = takedownExtensionList; - larger = loadedExtensionIDs; + larger = Array.from(loadedExtensionIDs.keys()); } else { - smaller = loadedExtensionIDs; + smaller = Array.from(loadedExtensionIDs.keys()); larger = takedownExtensionList; } const matches = []; for (const id of smaller) { - if (larger.has(id)) { + if (larger.has ? larger.has(id) : larger.includes(id)) { matches.push(id); } } @@ -521,7 +524,11 @@ define(function (require, exports, module) { } if(metadata.name) { - loadedExtensionIDs.add(metadata.name); + loadedExtensionIDs.set(metadata.name, { + loadedFromDisc: !!config.nativeDir, + extensionPath: config.nativeDir || config.baseUrl, + extensionName: metadata.title || metadata.name + }); } // No special handling for themes... Let the promise propagate into the ExtensionManager @@ -1045,10 +1052,21 @@ define(function (require, exports, module) { /** * Uninstall a deprecated extension * @param {string} extensionID - The ID of the extension to uninstall + * @return {Promise} A promise that resolves when the extension is uninstalled successfully */ - function uninstallExtension(extensionID) { - // TODO: Implement uninstall logic - alert(`Uninstall button clicked for extension: ${extensionID}`); + async function uninstallExtension(extensionID) { + const extensionInfo = loadedExtensionIDs.get(extensionID); + + if (!extensionInfo) { + throw new Error(`Extension ${extensionID} not found in loaded extensions`); + } + + if (!extensionInfo.loadedFromDisc) { + throw new Error(`Cannot uninstall built-in extension: ${extensionID}`); + } + + const extensionDir = FileSystem.getDirectoryForPath(extensionInfo.extensionPath); + await extensionDir.unlinkAsync(); } /** @@ -1057,6 +1075,7 @@ define(function (require, exports, module) { */ function _checkAndShowDeprecatedExtensionsDialog() { // Get deprecated extensions config + let needsRestart = false; const deprecatedExtensionsConfig = DefaultExtensions.deprecatedExtensions; if (!deprecatedExtensionsConfig || !deprecatedExtensionsConfig.extensionIDsAndDocs) { return; @@ -1072,10 +1091,12 @@ define(function (require, exports, module) { // Check which deprecated extensions are loaded and not yet shown const deprecatedExtensionsFound = []; - for (const extensionID of loadedExtensionIDs) { + for (const extensionID of loadedExtensionIDs.keys()) { if (deprecatedExtensionIDs[extensionID] && !shownDeprecatedExtensions[extensionID]) { + const extensionInfo = loadedExtensionIDs.get(extensionID); deprecatedExtensionsFound.push({ id: extensionID, + name: extensionInfo?.extensionName || extensionID, docUrl: deprecatedExtensionIDs[extensionID] }); } @@ -1093,12 +1114,53 @@ define(function (require, exports, module) { }; const $template = $(Mustache.render(DeprecatedExtensionsTemplate, templateVars)); - Dialogs.showModalDialogUsingTemplate($template); + const dialog = Dialogs.showModalDialogUsingTemplate($template, false); // autoDismiss = false // Wire up uninstall button click handlers - $template.on('click', '.uninstall-extension-btn', function() { - const extensionID = $(this).data('extension-id'); - uninstallExtension(extensionID); + $template.on('click', '.uninstall-extension-btn', async function() { + const $button = $(this); + const extensionID = $button.data('extension-id'); + const $extensionItem = $button.closest('.deprecated-extension-item'); + + // Disable button during uninstall + $button.prop('disabled', true); + $button.text(Strings.REMOVING); + + try { + Metrics.countEvent(Metrics.EVENT_TYPE.EXTENSIONS, "removeDep", extensionID); + await uninstallExtension(extensionID); + + // Update the OK button to "Restart App" + const $okButton = $template.find('[data-button-id="ok"]'); + $okButton.text(Strings.RESTART_APP_BUTTON); + + // Strike through the extension name and disable/strike the uninstall button + $extensionItem.find('.extension-info strong').addClass('striked'); + $button.remove(); + needsRestart = true; + } catch (err) { + Metrics.countEvent(Metrics.EVENT_TYPE.EXTENSIONS, "removeDep", "fail"); + logger.reportError(err, 'Failed to uninstall deprecated extension:' + extensionID); + + // Show error dialog + const message = StringUtils.format(Strings.ERROR_UNINSTALLING_EXTENSION_MESSAGE, extensionID); + Dialogs.showErrorDialog(Strings.ERROR_UNINSTALLING_EXTENSION_TITLE, message); + + // Re-enable button + $button.prop('disabled', false); + $button.text(Strings.REMOVE); + } + }); + + // Handle OK button click + $template.on('click', '[data-button-id="ok"]', function() { + if (needsRestart) { + // Reload the app to complete uninstallation + CommandManager.execute("debug.refreshWindow"); + } else { + // Just close the dialog + dialog.close(); + } }); // Mark each extension as shown From cdcf9e67b37bd3e42727b76a52a9f81f4c90839f Mon Sep 17 00:00:00 2001 From: abose Date: Sat, 25 Oct 2025 20:39:03 +0530 Subject: [PATCH 5/5] fix: prod build break --- src/utils/ExtensionLoader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 3d963570f8..01f5c2e4cd 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -1094,9 +1094,10 @@ define(function (require, exports, module) { for (const extensionID of loadedExtensionIDs.keys()) { if (deprecatedExtensionIDs[extensionID] && !shownDeprecatedExtensions[extensionID]) { const extensionInfo = loadedExtensionIDs.get(extensionID); + const extensionName = extensionInfo && extensionInfo.extensionName; deprecatedExtensionsFound.push({ id: extensionID, - name: extensionInfo?.extensionName || extensionID, + name: extensionName || extensionID, docUrl: deprecatedExtensionIDs[extensionID] }); }