/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.12
src/utils/ExtensionLoader.js
474 строки
19 KB
Martin Zagora
implement getDefaultExtensionPath function
03 мар 2017, 01:29
03 мар 2017, 01:29
7d54c14
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present 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"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /** * ExtensionLoader searches the filesystem for extensions, then creates a new context for each one and loads it. * This module dispatches the following events: * "load" - when an extension is successfully loaded. The second argument is the file path to the * extension root. * "loadFailed" - when an extension load is unsuccessful. The second argument is the file path to the * extension root. */ define(function (require, exports, module) { "use strict"; require("utils/Global"); var _ = require("thirdparty/lodash"), EventDispatcher = require("utils/EventDispatcher"), FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), Async = require("utils/Async"), ExtensionUtils = require("utils/ExtensionUtils"), UrlParams = require("utils/UrlParams").UrlParams, PathUtils = require("thirdparty/path-utils/path-utils"); // default async initExtension timeout var INIT_EXTENSION_TIMEOUT = 10000; var _init = false, _extensions = {}, _initExtensionTimeout = INIT_EXTENSION_TIMEOUT, srcPath = FileUtils.getNativeBracketsDirectoryPath(); /** * Stores require.js contexts of extensions * @type {Object.<string, Object>} */ var contexts = {}; // 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" // Retrieve the global paths var globalPaths = brackets._getGlobalRequireJSConfig().paths; // Convert the relative paths to absolute Object.keys(globalPaths).forEach(function (key) { globalPaths[key] = PathUtils.makePathAbsolute(srcPath + "/" + globalPaths[key]); }); /** * Returns the full path to the default extensions directory. */ function getDefaultExtensionPath() { return FileUtils.getNativeBracketsDirectoryPath() + "/extensions/default"; } /** * Returns the full path of the default user extensions directory. This is in the users * application support directory, which is typically * /Users/<user>/Application Support/Brackets/extensions/user on the mac, and * C:\Users\<user>\AppData\Roaming\Brackets\extensions\user on windows. */ function getUserExtensionPath() { if (brackets.app.getApplicationSupportDirectory) { return brackets.app.getApplicationSupportDirectory() + "/extensions/user"; } return null; } /** * Returns the require.js require context used to load an extension * * @param {!string} name, used to identify the extension * @return {!Object} A require.js require object used to load the extension, or undefined if * there is no require object with that name */ function getRequireContextForExtension(name) { return contexts[name]; } /** * @private * Get timeout value for rejecting an extension's async initExtension promise. * @return {number} Timeout in milliseconds */ function _getInitExtensionTimeout() { return _initExtensionTimeout; } /** * @private * Set timeout for rejecting an extension's async initExtension promise. * @param {number} value Timeout in milliseconds */ function _setInitExtensionTimeout(value) { _initExtensionTimeout = value; } /** * @private * Loads optional requirejs-config.json file for an extension * @param {Object} baseConfig * @return {$.Promise} */ function _mergeConfig(baseConfig) { var deferred = new $.Deferred(), extensionConfigFile = FileSystem.getFileForPath(baseConfig.baseUrl + "/requirejs-config.json"); // Optional JSON config for require.js FileUtils.readAsText(extensionConfigFile).done(function (text) { try { var extensionConfig = JSON.parse(text); // 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(); } /** * Loads the extension module that lives at baseUrl into its own Require.js context * * @param {!string} name, used to identify the extension * @param {!{baseUrl: string}} config object with baseUrl property containing absolute path of extension * @param {!string} entryPoint, name of the main js file to load * @return {!$.Promise} A promise object that is resolved when the extension is loaded, or rejected * if the extension fails to load or throws an exception immediately when loaded. * (Note: if extension contains a JS syntax error, promise is resolved not rejected). */ function loadExtensionModule(name, config, entryPoint) { var extensionConfig = { context: name, baseUrl: config.baseUrl, paths: globalPaths, locale: brackets.getLocale() }; // Read optional requirejs-config.json var promise = _mergeConfig(extensionConfig).then(function (mergedConfig) { // Create new RequireJS context and load extension entry point var extensionRequire = brackets.libRequire.config(mergedConfig), extensionRequireDeferred = new $.Deferred(); contexts[name] = extensionRequire; extensionRequire([entryPoint], extensionRequireDeferred.resolve, extensionRequireDeferred.reject); return extensionRequireDeferred.promise(); }).then(function (module) { // Extension loaded normally var initPromise; _extensions[name] = module; // Optional sync/async initExtension if (module && module.initExtension && (typeof module.initExtension === "function")) { // optional async extension init try { initPromise = Async.withTimeout(module.initExtension(), _getInitExtensionTimeout()); } catch (err) { // Synchronous error while initializing extension console.error("[Extension] Error -- error thrown during initExtension for " + name + ": " + err); return new $.Deferred().reject(err).promise(); } // initExtension may be synchronous and may not return a promise if (initPromise) { // WARNING: These calls to initPromise.fail() and initPromise.then(), // could also result in a runtime error if initPromise is not a valid // promise. Currently, the promise is wrapped via Async.withTimeout(), // so the call is safe as-is. initPromise.fail(function (err) { if (err === Async.ERROR_TIMEOUT) { console.error("[Extension] Error -- timeout during initExtension for " + name); } else { console.error("[Extension] Error -- failed initExtension for " + name + (err ? ": " + err : "")); } }); return initPromise; } } }, function errback(err) { // Extension failed to load during the initial require() call var additionalInfo = String(err); if (err.requireType === "scripterror" && err.originalError) { // This type has a misleading error message - replace it with something clearer (URL of require() call that got a 404 result) additionalInfo = "Module does not exist: " + err.originalError.target.src; } console.error("[Extension] failed to load " + config.baseUrl + " - " + additionalInfo); if (err.requireType === "define") { // This type has a useful stack (exception thrown by ext code or info on bad getModule() call) console.log(err.stack); } }); return promise; } /** * Loads the extension that lives at baseUrl into its own Require.js context * * @param {!string} name, used to identify the extension * @param {!{baseUrl: string}} config object with baseUrl property containing absolute path of extension * @param {!string} entryPoint, name of the main js file to load * @return {!$.Promise} A promise object that is resolved when the extension is loaded, or rejected * if the extension fails to load or throws an exception immediately when loaded. * (Note: if extension contains a JS syntax error, promise is resolved not rejected). */ function loadExtension(name, config, entryPoint) { 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); return promise .then(function (metadata) { // No special handling for themes... Let the promise propagate into the ExtensionManager if (metadata && metadata.theme) { return; } if (!metadata.disabled) { return loadExtensionModule(name, config, entryPoint); } else { return new $.Deferred().reject("disabled").promise(); } }) .then(function () { exports.trigger("load", config.baseUrl); }, function (err) { if (err === "disabled") { exports.trigger("disabled", config.baseUrl); } else { exports.trigger("loadFailed", config.baseUrl); } }); } /** * Runs unit tests for the extension that lives at baseUrl into its own Require.js context * * @param {!string} name, used to identify the extension * @param {!{baseUrl: string}} config object with baseUrl property containing absolute path of extension * @param {!string} entryPoint, name of the main js file to load * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function testExtension(name, config, entryPoint) { var result = new $.Deferred(), extensionPath = config.baseUrl + "/" + entryPoint + ".js"; FileSystem.resolve(extensionPath, function (err, entry) { if (!err && entry.isFile) { // unit test file exists var extensionRequire = brackets.libRequire.config({ context: name, baseUrl: config.baseUrl, paths: $.extend({}, config.paths, globalPaths) }); extensionRequire([entryPoint], function () { result.resolve(); }); } else { result.reject(); } }); return result.promise(); } /** * @private * Loads a file entryPoint from each extension folder within the baseUrl into its own Require.js context * * @param {!string} directory, an absolute native path that contains a directory of extensions. * each subdirectory is interpreted as an independent extension * @param {!{baseUrl: string}} config object with baseUrl property containing absolute path of extension folder * @param {!string} entryPoint Module name to load (without .js suffix) * @param {function} processExtension * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function _loadAll(directory, config, entryPoint, processExtension) { var result = new $.Deferred(); FileSystem.getDirectoryForPath(directory).getContents(function (err, contents) { if (!err) { var i, extensions = []; for (i = 0; i < contents.length; i++) { if (contents[i].isDirectory) { // FUTURE (JRB): read package.json instead of just using the entrypoint "main". // Also, load sub-extensions defined in package.json. extensions.push(contents[i].name); } } if (extensions.length === 0) { result.resolve(); return; } Async.doInParallel(extensions, function (item) { var extConfig = { baseUrl: config.baseUrl + "/" + item, paths: config.paths }; return processExtension(item, extConfig, entryPoint); }).always(function () { // Always resolve the promise even if some extensions had errors result.resolve(); }); } else { console.error("[Extension] Error -- could not read native directory: " + directory); result.reject(); } }); return result.promise(); } /** * Loads the extension that lives at baseUrl into its own Require.js context * * @param {!string} directory, an absolute native path that contains a directory of extensions. * each subdirectory is interpreted as an independent extension * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function loadAllExtensionsInNativeDirectory(directory) { return _loadAll(directory, {baseUrl: directory}, "main", loadExtension); } /** * Runs unit test for the extension that lives at baseUrl into its own Require.js context * * @param {!string} directory, an absolute native path that contains a directory of extensions. * each subdirectory is interpreted as an independent extension * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function testAllExtensionsInNativeDirectory(directory) { var bracketsPath = FileUtils.getNativeBracketsDirectoryPath(), config = { baseUrl: directory }; config.paths = { "perf": bracketsPath + "/perf", "spec": bracketsPath + "/spec" }; return _loadAll(directory, config, "unittests", testExtension); } /** * Load extensions. * * @param {?Array.<string>} A list containing references to extension source * location. A source location may be either (a) a folder name inside * src/extensions or (b) an absolute path. * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function init(paths) { var params = new UrlParams(); if (_init) { // Only init once. Return a resolved promise. return new $.Deferred().resolve().promise(); } if (!paths) { params.parse(); if (params.get("reloadWithoutUserExts") === "true") { paths = ["default"]; } else { paths = [ getDefaultExtensionPath(), "dev", getUserExtensionPath() ]; } } // Load extensions before restoring the project // Get a Directory for the user extension directory and create it if it doesn't exist. // Note that this is an async call and there are no success or failure functions passed // in. If the directory *doesn't* exist, it will be created. Extension loading may happen // before the directory is finished being created, but that is okay, since the extension // loading will work correctly without this directory. // If the directory *does* exist, nothing else needs to be done. It will be scanned normally // during extension loading. var extensionPath = getUserExtensionPath(); FileSystem.getDirectoryForPath(extensionPath).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; } return loadAllExtensionsInNativeDirectory(extensionPath); }, false); promise.always(function () { _init = true; }); return promise; } EventDispatcher.makeEventDispatcher(exports); // unit tests exports._setInitExtensionTimeout = _setInitExtensionTimeout; exports._getInitExtensionTimeout = _getInitExtensionTimeout; // public API exports.init = init; exports.getDefaultExtensionPath = getDefaultExtensionPath; exports.getUserExtensionPath = getUserExtensionPath; exports.getRequireContextForExtension = getRequireContextForExtension; exports.loadExtension = loadExtension; exports.testExtension = testExtension; exports.loadAllExtensionsInNativeDirectory = loadAllExtensionsInNativeDirectory; exports.testAllExtensionsInNativeDirectory = testAllExtensionsInNativeDirectory; });