diff --git a/src/extensions/default/DefaultExtensions.json b/src/extensions/default/DefaultExtensions.json new file mode 100644 index 0000000000..ba6804a5dd --- /dev/null +++ b/src/extensions/default/DefaultExtensions.json @@ -0,0 +1,3 @@ +[ + "CodeFolding" +] diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 571cde1e78..b25677408f 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -1,5 +1,6 @@ /* - * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. + * Copyright (c) 2021 - present core.ai . All rights reserved. + * Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -29,6 +30,11 @@ * "loadFailed" - when an extension load is unsuccessful. The second argument is the file path to the * extension root. */ +// jshint ignore: start +/*global fs, Phoenix*/ +/*eslint-env es6*/ +/*eslint no-console: 0*/ +/*eslint strict: ["error", "global"]*/ define(function (require, exports, module) { @@ -58,6 +64,8 @@ define(function (require, exports, module) { */ var contexts = {}; + var pathLib = Phoenix.VFS.path; + // The native directory path ends with either "test" or "src". We need "src" to // load the text and i18n modules. srcPath = srcPath.replace(/\/test$/, "/src"); // convert from "test" to "src" @@ -72,10 +80,17 @@ define(function (require, exports, module) { }); /** - * Returns the full path to the default extensions directory. + * Returns the path to the default extensions directory relative to window.location.href */ function getDefaultExtensionPath() { - return FileUtils.getNativeBracketsDirectoryPath() + "/extensions/default"; + return pathLib.normalize("/extensions/default"); + } + + /** + * Returns the full path to the development extensions directory. + */ + function getDevExtensionPath() { + return pathLib.normalize(brackets.app.getApplicationSupportDirectory() + "/extensions/dev"); } /** @@ -86,7 +101,7 @@ define(function (require, exports, module) { */ function getUserExtensionPath() { if (brackets.app.getApplicationSupportDirectory) { - return brackets.app.getApplicationSupportDirectory() + "/extensions/user"; + return pathLib.normalize(brackets.app.getApplicationSupportDirectory() + "/extensions/user"); } return null; @@ -121,6 +136,38 @@ define(function (require, exports, module) { _initExtensionTimeout = value; } + /** + * @private + * Loads optional requirejs-config.json file for an extension + * @param {Object} baseConfig + * @return {$.Promise} + */ + function _mergeConfigFromURL(baseConfig) { + var deferred = new $.Deferred(), + extensionConfigFile = baseConfig.baseUrl + "/requirejs-config.json"; + + // Optional JSON config for require.js + $.get(extensionConfigFile).done(function (extensionConfig) { + try { + // baseConfig.paths properties will override any extension config paths + _.extend(extensionConfig.paths, baseConfig.paths); + + // Overwrite baseUrl, context, locale (paths is already merged above) + _.extend(extensionConfig, _.omit(baseConfig, "paths")); + + deferred.resolve(extensionConfig); + } catch (err) { + // Failed to parse requirejs-config.json + deferred.reject("failed to parse requirejs-config.json"); + } + }).fail(function () { + // If requirejs-config.json isn't specified, resolve with the baseConfig only + deferred.resolve(baseConfig); + }); + + return deferred.promise(); + } + /** * @private * Loads optional requirejs-config.json file for an extension @@ -128,6 +175,9 @@ define(function (require, exports, module) { * @return {$.Promise} */ function _mergeConfig(baseConfig) { + if(baseConfig.baseUrl.startsWith("http://") || baseConfig.baseUrl.startsWith("https://")) { + return _mergeConfigFromURL(baseConfig); + } var deferred = new $.Deferred(), extensionConfigFile = FileSystem.getFileForPath(baseConfig.baseUrl + "/requirejs-config.json"); @@ -249,7 +299,7 @@ define(function (require, exports, module) { var promise = new $.Deferred(); // Try to load the package.json to figure out if we are loading a theme. - ExtensionUtils.loadMetadata(config.baseUrl).always(promise.resolve); + ExtensionUtils.loadMetadata(config.baseUrl, name).always(promise.resolve); return promise .then(function (metadata) { @@ -358,6 +408,40 @@ define(function (require, exports, module) { return result.promise(); } + /** + * Loads All brackets default extensions from brackets base https URL. + * + * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. + */ + function loadAllDefaultExtensions() { + const extensionPath = getDefaultExtensionPath(); + const href = window.location.href; + const baseUrl = href.substring(0, href.lastIndexOf("/")); + const extensionsToLoadURL = baseUrl + extensionPath + "/DefaultExtensions.json"; + var result = new $.Deferred(); + + $.get(extensionsToLoadURL).done(function (extensionNames) { + Async.doInParallel(extensionNames, function (extensionName) { + console.log("loading default extension: ", extensionName); + var extConfig = { + baseUrl: baseUrl + extensionPath + "/" + extensionName + }; + return loadExtension(extensionName, extConfig, 'main'); + }).always(function () { + // Always resolve the promise even if some extensions had errors + result.resolve(); + }); + + }) + .fail(function (err) { + console.error("[Extension] Error -- could not read default extension list from" + extensionsToLoadURL); + result.reject(); + }); + + return result.promise(); + + } + /** * Loads the extension that lives at baseUrl into its own Require.js context * @@ -409,13 +493,10 @@ define(function (require, exports, module) { if (!paths) { params.parse(); - if (params.get("reloadWithoutUserExts") === "true") { - paths = ["default"]; - } else { + if (params.get("reloadWithoutUserExts") !== "true") { paths = [ - getDefaultExtensionPath(), - "dev", - getUserExtensionPath() + getUserExtensionPath(), + getDevExtensionPath() ]; } } @@ -431,20 +512,15 @@ define(function (require, exports, module) { // during extension loading. var extensionPath = getUserExtensionPath(); FileSystem.getDirectoryForPath(extensionPath).create(); + FileSystem.getDirectoryForPath(getDevExtensionPath()).create(); // Create the extensions/disabled directory, too. var disabledExtensionPath = extensionPath.replace(/\/user$/, "/disabled"); FileSystem.getDirectoryForPath(disabledExtensionPath).create(); - var promise = Async.doSequentially(paths, function (item) { - var extensionPath = item; - - // If the item has "/" in it, assume it is a full path. Otherwise, load - // from our source path + "/extensions/". - if (item.indexOf("/") === -1) { - extensionPath = FileUtils.getNativeBracketsDirectoryPath() + "/extensions/" + item; - } + loadAllDefaultExtensions(); + var promise = Async.doSequentially(paths, function (extensionPath) { return loadAllExtensionsInNativeDirectory(extensionPath); }, false); diff --git a/src/utils/ExtensionUtils.js b/src/utils/ExtensionUtils.js index 39021e9443..841231d56f 100644 --- a/src/utils/ExtensionUtils.js +++ b/src/utils/ExtensionUtils.js @@ -151,17 +151,7 @@ define(function (require, exports, module) { * @return {!string} The URL to the module's folder **/ function getModuleUrl(module, path) { - var url = encodeURI(getModulePath(module, path)); - - // On Windows, $.get() fails if the url is a full pathname. To work around this, - // prepend "file:///". On the Mac, $.get() works fine if the url is a full pathname, - // but *doesn't* work if it is prepended with "file://". Go figure. - // However, the prefix "file://localhost" does work. - if (brackets.platform === "win" && url.indexOf(":") !== -1) { - url = "file:///" + url; - } - - return url; + return encodeURI(getModulePath(module, path)); } /** @@ -238,7 +228,7 @@ define(function (require, exports, module) { * @return {$.Promise} A promise object that is resolved with the parsed contents of the package.json file, * or rejected if there is no package.json with the boolean indicating whether .disabled file exists. */ - function loadMetadata(folder) { + function _loadLocalMetadata(folder) { var packageJSONFile = FileSystem.getFileForPath(folder + "/package.json"), disabledFile = FileSystem.getFileForPath(folder + "/.disabled"), baseName = FileUtils.getBaseName(folder), @@ -287,6 +277,61 @@ define(function (require, exports, module) { }); return result.promise(); } + /** + * Loads the package.json file in the given extension folder as well as any additional + * metadata. + * + * @param {string} baseExtensionUrl The extension folder. + * @param {?string} extensionName optional extension name + * @return {$.Promise} A promise object that is resolved with the parsed contents of the package.json file, + * or rejected if there is no package.json with the boolean indicating whether .disabled file exists. + */ + function _loadDefaultExtensionMetadata(baseExtensionUrl, extensionName) { + var packageJSONFile = baseExtensionUrl + "/package.json"; + var result = new $.Deferred(); + var json = { + name: extensionName + }; + $.get(packageJSONFile) + .then(function (result) { + json = result; + }).always(function () { + // if we don't have any metadata for the extension + // we should still create an empty one, so we can attach + // disabled property on it in case it's disabled + var disabled; + var defaultDisabled = PreferencesManager.get("extensions.default.disabled"); + if (Array.isArray(defaultDisabled) && defaultDisabled.indexOf(extensionName) !== -1) { + console.warn("Default extension has been disabled on startup: " + baseExtensionUrl); + disabled = true; + } + json.disabled = disabled; + result.resolve(json); + }); + + return result.promise(); + } + + /** + * Loads the package.json file in the given extension folder as well as any additional + * metadata for default extensions in the source directory. + * + * If there's a .disabled file in the extension directory, then the content of package.json + * will be augmented with disabled property set to true. It will override whatever value of + * disabled might be set. + * + * @param {string} folder The extension folder/base url for default extensions. + * @return {$.Promise} A promise object that is resolved with the parsed contents of the package.json file, + * or rejected if there is no package.json with the boolean indicating whether .disabled file exists. + */ + function loadMetadata(folder, extensionName) { + if(folder.startsWith("http://") || folder.startsWith("https://")) { + return _loadDefaultExtensionMetadata(folder, extensionName); + } + return _loadLocalMetadata(folder); + } + + exports.addEmbeddedStyleSheet = addEmbeddedStyleSheet; exports.addLinkedStyleSheet = addLinkedStyleSheet;