diff --git a/src/extensibility/ExtensionManager.js b/src/extensibility/ExtensionManager.js index ebc8b38613e..00f67ca5b68 100644 --- a/src/extensibility/ExtensionManager.js +++ b/src/extensibility/ExtensionManager.js @@ -22,7 +22,7 @@ */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, window, $, brackets */ +/*global define, window, $, brackets, semver */ /*unittests: ExtensionManager*/ /** @@ -42,6 +42,9 @@ define(function (require, exports, module) { NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, ExtensionLoader = require("utils/ExtensionLoader"); + // semver isn't a proper AMD module, so it will just load into the global namespace. + require("extensibility/node/node_modules/semver/semver"); + /** * Extension status constants. */ @@ -85,7 +88,11 @@ define(function (require, exports, module) { */ function getRegistry(forceDownload) { if (!_registry || forceDownload) { - return $.getJSON(brackets.config.extension_registry, {cache: false}) + return $.ajax({ + url: brackets.config.extension_registry, + dataType: "json", + cache: false + }) .done(function (data) { _registry = data; }); @@ -155,12 +162,45 @@ define(function (require, exports, module) { return (_extensions[id] && _extensions[id].status) || NOT_INSTALLED; } + /** + * Returns information about whether the given entry is compatible with the given Brackets API version. + * @param {Object} entry The registry entry to check. + * @param {string} apiVersion The Brackets API version to check against. + * @return {{isCompatible: boolean, requiresNewer}} Result contains an + * "isCompatible" member saying whether it's compatible. If not compatible, then + * "requiresNewer" says whether it requires an older or newer version of Brackets. + */ + function getCompatibilityInfo(entry, apiVersion) { + var requiredVersion = entry.metadata.engines && entry.metadata.engines.brackets, + result = {}; + result.isCompatible = !requiredVersion || semver.satisfies(apiVersion, requiredVersion); + if (!result.isCompatible) { + if (requiredVersion.charAt(0) === '<') { + result.requiresNewer = false; + } else if (requiredVersion.charAt(0) === '>') { + result.requiresNewer = true; + } else if (requiredVersion.charAt(0) === "~") { + var compareVersion = requiredVersion.slice(1); + // Need to add .0s to this style of range in order to compare (since valid version + // numbers must have major/minor/patch). + if (compareVersion.match(/^[0-9]+$/)) { + compareVersion += ".0.0"; + } else if (compareVersion.match(/^[0-9]+\.[0-9]+$/)) { + compareVersion += ".0"; + } + result.requiresNewer = semver.lt(apiVersion, compareVersion); + } + } + return result; + } + // Listen to extension load events $(ExtensionLoader).on("load", _handleExtensionLoad); // Public exports exports.getRegistry = getRegistry; exports.getStatus = getStatus; + exports.getCompatibilityInfo = getCompatibilityInfo; exports.NOT_INSTALLED = NOT_INSTALLED; exports.ENABLED = ENABLED; diff --git a/src/extensibility/ExtensionManagerView.js b/src/extensibility/ExtensionManagerView.js index c966b01fa51..6365c5d8d49 100644 --- a/src/extensibility/ExtensionManagerView.js +++ b/src/extensibility/ExtensionManagerView.js @@ -22,7 +22,7 @@ */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, window, $, brackets, Mustache, semver */ +/*global define, window, $, brackets, Mustache */ /*unittests: ExtensionManager*/ define(function (require, exports, module) { @@ -36,12 +36,11 @@ define(function (require, exports, module) { registry_utils = require("extensibility/registry_utils"), itemTemplate = require("text!htmlContent/extension-manager-view-item.html"); - // semver isn't a proper AMD module, so it will just load into the global namespace. - require("extensibility/node/node_modules/semver/semver"); - /** * @constructor * Creates a view enabling the user to install and manage extensions. + * Events: + * "render": whenever the view fully renders itself. */ function ExtensionManagerView() { var self = this; @@ -81,7 +80,7 @@ define(function (require, exports, module) { // Show the busy spinner and access the registry. var $spinner = $("
") .appendTo(this.$el); - ExtensionManager.getRegistry().done(function (registry) { + ExtensionManager.getRegistry(true).done(function (registry) { // Display the registry view. self._render(registry_utils.sortRegistry(registry)); }).fail(function () { @@ -116,10 +115,17 @@ define(function (require, exports, module) { // Create a Mustache context object containing the entry data and our helper functions. var context = $.extend({}, entry), status = ExtensionManager.getStatus(entry.metadata.name); + + // Normally we would merge the strings into the context we're passing into the template, + // but since we're instantiating the template for every item, it seems wrong to take the hit + // of copying all the strings into the context, so we just make it a subfield. + context.Strings = Strings; + context.isInstalled = (status === ExtensionManager.ENABLED); - var requiredVersion = entry.metadata.engines && entry.metadata.engines.brackets; - context.isCompatible = !requiredVersion || semver.satisfies(brackets.metadata.apiVersion, requiredVersion); + var compatInfo = ExtensionManager.getCompatibilityInfo(entry, brackets.metadata.apiVersion); + context.isCompatible = compatInfo.isCompatible; + context.requiresNewer = compatInfo.requiresNewer; context.allowInstall = context.isCompatible && !context.isInstalled; @@ -146,6 +152,7 @@ define(function (require, exports, module) { $item.appendTo($table); }); $table.appendTo(this.$el); + $(this).triggerHandler("render"); }; /** diff --git a/src/htmlContent/extension-manager-view-item.html b/src/htmlContent/extension-manager-view-item.html index dbefa7342e6..a58b529409f 100644 --- a/src/htmlContent/extension-manager-view-item.html +++ b/src/htmlContent/extension-manager-view-item.html @@ -3,27 +3,30 @@ {{#metadata.title}}{{metadata.title}}{{/metadata.title}}{{^metadata.title}}{{metadata.name}}{{/metadata.title}} {{metadata.version}}
- by + {{Strings.EXTENSION_AUTHOR}}: {{#metadata.author.name}} {{metadata.author.name}} / {{/metadata.author.name}} {{formatUserId}} - - on {{lastVersionDate}} +
+ {{Strings.EXTENSION_DATE}}: {{lastVersionDate}} {{^isCompatible}} -
This extension is incompatible with this version of Brackets.
+
+ {{#requiresNewer}}{{Strings.EXTENSION_INCOMPATIBLE_NEWER}}{{/requiresNewer}} + {{^requiresNewer}}{{Strings.EXTENSION_INCOMPATIBLE_OLDER}}{{/requiresNewer}} +
{{/isCompatible}} {{#metadata.description}} {{metadata.description}} {{/metadata.description}} {{^metadata.description}} - No description + {{Strings.EXTENSION_NO_DESCRIPTION}} {{/metadata.description}} {{#metadata.keywords.length}}
- Keywords: + {{Strings.EXTENSION_KEYWORDS}}: {{#metadata.keywords}} {{.}} {{/metadata.keywords}} @@ -32,8 +35,8 @@ diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 05df2108126..c88fb586b0f 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -323,7 +323,7 @@ define({ "INVALID_VERSION_NUMBER" : "The package version number ({0}) is invalid.", "INVALID_BRACKETS_VERSION" : "The Brackets compatibility string {{0}} is invalid.", "DISALLOWED_WORDS" : "The words {{1}} are not allowed in the {{0}} field.", - "API_NOT_COMPATIBLE" : "The extension isn't compatible with this version of Brackets. It's installed in your disabled extensions folder.", + "API_NOT_COMPATIBLE" : "The extension isn't compatible with this version of {APP_NAME}. It's installed in your disabled extensions folder.", "MISSING_MAIN" : "The package has no main.js file.", "ALREADY_INSTALLED" : "An extension with the same name was already installed. The new extension is installed in your disabled extensions folder.", "NO_DISABLED_DIRECTORY" : "Cannot save extension to extensions/disabled because the folder does not exist.", @@ -337,8 +337,16 @@ define({ "UNKNOWN_ERROR" : "Unknown internal error.", // For NOT_FOUND_ERR, see generic strings above "EXTENSION_MANAGER_TITLE" : "Extension Manager", - "EXTENSION_MANAGER_ERROR_LOAD" : "Unable to access the Brackets extension registry. Please try again later.", + "EXTENSION_MANAGER_ERROR_LOAD" : "Unable to access the extension registry. Please try again later.", "INSTALL_FROM_URL" : "Install from URL\u2026", + "EXTENSION_AUTHOR" : "Author", + "EXTENSION_DATE" : "Date", + "EXTENSION_INCOMPATIBLE_NEWER" : "This extension requires a newer version of {APP_NAME}.", + "EXTENSION_INCOMPATIBLE_OLDER" : "This extension currently only works with older versions of {APP_NAME}.", + "EXTENSION_NO_DESCRIPTION" : "No description", + "EXTENSION_KEYWORDS" : "Keywords", + "EXTENSION_INSTALLED" : "Installed", + "EXTENSION_SEARCH_PLACEHOLDER" : "Search", // extensions/default/JSLint "JSLINT_ERRORS" : "JSLint Errors", diff --git a/test/spec/ExtensionManager-test.js b/test/spec/ExtensionManager-test.js index c9c765182fb..6c04d5af9ae 100644 --- a/test/spec/ExtensionManager-test.js +++ b/test/spec/ExtensionManager-test.js @@ -224,6 +224,37 @@ define(function (require, exports, module) { expect(spy).toHaveBeenCalledWith(jasmine.any(Object), mockPath + "/mock-legacy-extension", ExtensionManager.ENABLED); }); }); + + it("should calculate compatibility info correctly", function () { + function fakeEntry(version) { + return { metadata: { engines: { brackets: version } } }; + } + + expect(ExtensionManager.getCompatibilityInfo(fakeEntry(null), "1.0.0")) + .toEqual({isCompatible: true}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry(">0.5.0"), "0.6.0")) + .toEqual({isCompatible: true}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry(">0.6.0"), "0.6.0")) + .toEqual({isCompatible: false, requiresNewer: true}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry(">0.7.0"), "0.6.0")) + .toEqual({isCompatible: false, requiresNewer: true}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("<0.5.0"), "0.4.0")) + .toEqual({isCompatible: true}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("<0.4.0"), "0.4.0")) + .toEqual({isCompatible: false, requiresNewer: false}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("<0.3.0"), "0.4.0")) + .toEqual({isCompatible: false, requiresNewer: false}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.2.0")) + .toEqual({isCompatible: true}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.2.1")) + .toEqual({isCompatible: true}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.3.0")) + .toEqual({isCompatible: false, requiresNewer: false}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.3.1")) + .toEqual({isCompatible: false, requiresNewer: false}); + expect(ExtensionManager.getCompatibilityInfo(fakeEntry("~1.2"), "1.1.0")) + .toEqual({isCompatible: false, requiresNewer: true}); + }); }); describe("ExtensionManagerView", function () { @@ -231,14 +262,14 @@ define(function (require, exports, module) { // Sets up a real registry (with mock data). function setupRegistryWithMockLoad() { - // Prefetch the model so the view is constructed immediately. (mockjax appears to - // add a little asynchronicity even if it's returning data right away.) - runs(function () { - waitsForDone(ExtensionManager.getRegistry()); - }); + var rendered = false; runs(function () { view = new ExtensionManagerView(); + $(view).on("render", function () { + rendered = true; + }); }); + waitsFor(function () { return rendered; }, "view rendering"); } // Sets up a mock registry (with no data).