diff --git a/src/package-manager.coffee b/src/package-manager.coffee deleted file mode 100644 index 55c5f2f02..000000000 --- a/src/package-manager.coffee +++ /dev/null @@ -1,652 +0,0 @@ -path = require 'path' -normalizePackageData = null - -_ = require 'underscore-plus' -{Emitter} = require 'event-kit' -fs = require 'fs-plus' -CSON = require 'season' - -ServiceHub = require 'service-hub' -Package = require './package' -ThemePackage = require './theme-package' -{isDeprecatedPackage, getDeprecatedPackageMetadata} = require './deprecated-packages' -packageJSON = require('../package.json') - -# Extended: Package manager for coordinating the lifecycle of Atom packages. -# -# An instance of this class is always available as the `atom.packages` global. -# -# Packages can be loaded, activated, and deactivated, and unloaded: -# * Loading a package reads and parses the package's metadata and resources -# such as keymaps, menus, stylesheets, etc. -# * Activating a package registers the loaded resources and calls `activate()` -# on the package's main module. -# * Deactivating a package unregisters the package's resources and calls -# `deactivate()` on the package's main module. -# * Unloading a package removes it completely from the package manager. -# -# Packages can be enabled/disabled via the `core.disabledPackages` config -# settings and also by calling `enablePackage()/disablePackage()`. -module.exports = -class PackageManager - constructor: (params) -> - { - @config, @styleManager, @notificationManager, @keymapManager, - @commandRegistry, @grammarRegistry, @deserializerManager, @viewRegistry - } = params - - @emitter = new Emitter - @activationHookEmitter = new Emitter - @packageDirPaths = [] - @deferredActivationHooks = [] - @triggeredActivationHooks = new Set() - @packagesCache = packageJSON._atomPackages ? {} - @packageDependencies = packageJSON.packageDependencies ? {} - @initialPackagesLoaded = false - @initialPackagesActivated = false - @preloadedPackages = {} - @loadedPackages = {} - @activePackages = {} - @activatingPackages = {} - @packageStates = {} - @serviceHub = new ServiceHub - - @packageActivators = [] - @registerPackageActivator(this, ['atom', 'textmate']) - - initialize: (params) -> - {configDirPath, @devMode, safeMode, @resourcePath} = params - if configDirPath? and not safeMode - if @devMode - @packageDirPaths.push(path.join(configDirPath, "dev", "packages")) - @packageDirPaths.push(path.join(configDirPath, "packages")) - - setContextMenuManager: (@contextMenuManager) -> - - setMenuManager: (@menuManager) -> - - setThemeManager: (@themeManager) -> - - reset: -> - @serviceHub.clear() - @deactivatePackages() - @loadedPackages = {} - @preloadedPackages = {} - @packageStates = {} - @packagesCache = packageJSON._atomPackages ? {} - @packageDependencies = packageJSON.packageDependencies ? {} - @triggeredActivationHooks.clear() - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when all packages have been loaded. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidLoadInitialPackages: (callback) -> - @emitter.on 'did-load-initial-packages', callback - - # Public: Invoke the given callback when all packages have been activated. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidActivateInitialPackages: (callback) -> - @emitter.on 'did-activate-initial-packages', callback - - # Public: Invoke the given callback when a package is activated. - # - # * `callback` A {Function} to be invoked when a package is activated. - # * `package` The {Package} that was activated. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidActivatePackage: (callback) -> - @emitter.on 'did-activate-package', callback - - # Public: Invoke the given callback when a package is deactivated. - # - # * `callback` A {Function} to be invoked when a package is deactivated. - # * `package` The {Package} that was deactivated. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDeactivatePackage: (callback) -> - @emitter.on 'did-deactivate-package', callback - - # Public: Invoke the given callback when a package is loaded. - # - # * `callback` A {Function} to be invoked when a package is loaded. - # * `package` The {Package} that was loaded. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidLoadPackage: (callback) -> - @emitter.on 'did-load-package', callback - - # Public: Invoke the given callback when a package is unloaded. - # - # * `callback` A {Function} to be invoked when a package is unloaded. - # * `package` The {Package} that was unloaded. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidUnloadPackage: (callback) -> - @emitter.on 'did-unload-package', callback - - ### - Section: Package system data - ### - - # Public: Get the path to the apm command. - # - # Uses the value of the `core.apmPath` config setting if it exists. - # - # Return a {String} file path to apm. - getApmPath: -> - configPath = atom.config.get('core.apmPath') - return configPath if configPath - return @apmPath if @apmPath? - - commandName = 'apm' - commandName += '.cmd' if process.platform is 'win32' - apmRoot = path.join(process.resourcesPath, 'app', 'apm') - @apmPath = path.join(apmRoot, 'bin', commandName) - unless fs.isFileSync(@apmPath) - @apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName) - @apmPath - - # Public: Get the paths being used to look for packages. - # - # Returns an {Array} of {String} directory paths. - getPackageDirPaths: -> - _.clone(@packageDirPaths) - - ### - Section: General package data - ### - - # Public: Resolve the given package name to a path on disk. - # - # * `name` - The {String} package name. - # - # Return a {String} folder path or undefined if it could not be resolved. - resolvePackagePath: (name) -> - return name if fs.isDirectorySync(name) - - packagePath = fs.resolve(@packageDirPaths..., name) - return packagePath if fs.isDirectorySync(packagePath) - - packagePath = path.join(@resourcePath, 'node_modules', name) - return packagePath if @hasAtomEngine(packagePath) - - # Public: Is the package with the given name bundled with Atom? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isBundledPackage: (name) -> - @getPackageDependencies().hasOwnProperty(name) - - isDeprecatedPackage: (name, version) -> - isDeprecatedPackage(name, version) - - getDeprecatedPackageMetadata: (name) -> - getDeprecatedPackageMetadata(name) - - ### - Section: Enabling and disabling packages - ### - - # Public: Enable the package with the given name. - # - # * `name` - The {String} package name. - # - # Returns the {Package} that was enabled or null if it isn't loaded. - enablePackage: (name) -> - pack = @loadPackage(name) - pack?.enable() - pack - - # Public: Disable the package with the given name. - # - # * `name` - The {String} package name. - # - # Returns the {Package} that was disabled or null if it isn't loaded. - disablePackage: (name) -> - pack = @loadPackage(name) - - unless @isPackageDisabled(name) - pack?.disable() - - pack - - # Public: Is the package with the given name disabled? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isPackageDisabled: (name) -> - _.include(@config.get('core.disabledPackages') ? [], name) - - ### - Section: Accessing active packages - ### - - # Public: Get an {Array} of all the active {Package}s. - getActivePackages: -> - _.values(@activePackages) - - # Public: Get the active {Package} with the given name. - # - # * `name` - The {String} package name. - # - # Returns a {Package} or undefined. - getActivePackage: (name) -> - @activePackages[name] - - # Public: Is the {Package} with the given name active? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isPackageActive: (name) -> - @getActivePackage(name)? - - # Public: Returns a {Boolean} indicating whether package activation has occurred. - hasActivatedInitialPackages: -> @initialPackagesActivated - - ### - Section: Accessing loaded packages - ### - - # Public: Get an {Array} of all the loaded {Package}s - getLoadedPackages: -> - _.values(@loadedPackages) - - # Get packages for a certain package type - # - # * `types` an {Array} of {String}s like ['atom', 'textmate']. - getLoadedPackagesForTypes: (types) -> - pack for pack in @getLoadedPackages() when pack.getType() in types - - # Public: Get the loaded {Package} with the given name. - # - # * `name` - The {String} package name. - # - # Returns a {Package} or undefined. - getLoadedPackage: (name) -> - @loadedPackages[name] - - # Public: Is the package with the given name loaded? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isPackageLoaded: (name) -> - @getLoadedPackage(name)? - - # Public: Returns a {Boolean} indicating whether package loading has occurred. - hasLoadedInitialPackages: -> @initialPackagesLoaded - - ### - Section: Accessing available packages - ### - - # Public: Returns an {Array} of {String}s of all the available package paths. - getAvailablePackagePaths: -> - @getAvailablePackages().map((a) -> a.path) - - # Public: Returns an {Array} of {String}s of all the available package names. - getAvailablePackageNames: -> - @getAvailablePackages().map((a) -> a.name) - - # Public: Returns an {Array} of {String}s of all the available package metadata. - getAvailablePackageMetadata: -> - packages = [] - for pack in @getAvailablePackages() - metadata = @getLoadedPackage(pack.name)?.metadata ? @loadPackageMetadata(pack, true) - packages.push(metadata) - packages - - getAvailablePackages: -> - packages = [] - packagesByName = new Set() - - for packageDirPath in @packageDirPaths - if fs.isDirectorySync(packageDirPath) - for packagePath in fs.readdirSync(packageDirPath) - packagePath = path.join(packageDirPath, packagePath) - packageName = path.basename(packagePath) - if not packageName.startsWith('.') and not packagesByName.has(packageName) and fs.isDirectorySync(packagePath) - packages.push({ - name: packageName, - path: packagePath, - isBundled: false - }) - packagesByName.add(packageName) - - for packageName of @packageDependencies - unless packagesByName.has(packageName) - packages.push({ - name: packageName, - path: path.join(@resourcePath, 'node_modules', packageName), - isBundled: true - }) - - packages.sort((a, b) -> a.name.toLowerCase().localeCompare(b.name.toLowerCase())) - - ### - Section: Private - ### - - getPackageState: (name) -> - @packageStates[name] - - setPackageState: (name, state) -> - @packageStates[name] = state - - getPackageDependencies: -> - @packageDependencies - - hasAtomEngine: (packagePath) -> - metadata = @loadPackageMetadata(packagePath, true) - metadata?.engines?.atom? - - unobserveDisabledPackages: -> - @disabledPackagesSubscription?.dispose() - @disabledPackagesSubscription = null - - observeDisabledPackages: -> - @disabledPackagesSubscription ?= @config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) => - packagesToEnable = _.difference(oldValue, newValue) - packagesToDisable = _.difference(newValue, oldValue) - - @deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName) - @activatePackage(packageName) for packageName in packagesToEnable - null - - unobservePackagesWithKeymapsDisabled: -> - @packagesWithKeymapsDisabledSubscription?.dispose() - @packagesWithKeymapsDisabledSubscription = null - - observePackagesWithKeymapsDisabled: -> - @packagesWithKeymapsDisabledSubscription ?= @config.onDidChange 'core.packagesWithKeymapsDisabled', ({newValue, oldValue}) => - keymapsToEnable = _.difference(oldValue, newValue) - keymapsToDisable = _.difference(newValue, oldValue) - - disabledPackageNames = new Set(@config.get('core.disabledPackages')) - for packageName in keymapsToDisable when not disabledPackageNames.has(packageName) - @getLoadedPackage(packageName)?.deactivateKeymaps() - for packageName in keymapsToEnable when not disabledPackageNames.has(packageName) - @getLoadedPackage(packageName)?.activateKeymaps() - null - - preloadPackages: -> - for packageName, pack of @packagesCache - @preloadPackage(packageName, pack) - - preloadPackage: (packageName, pack) -> - metadata = pack.metadata ? {} - unless typeof metadata.name is 'string' and metadata.name.length > 0 - metadata.name = packageName - - if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string' - metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '') - - options = { - path: pack.rootDirPath, name: packageName, preloadedPackage: true, - bundledPackage: true, metadata, packageManager: this, @config, - @styleManager, @commandRegistry, @keymapManager, - @notificationManager, @grammarRegistry, @themeManager, @menuManager, - @contextMenuManager, @deserializerManager, @viewRegistry - } - if metadata.theme - pack = new ThemePackage(options) - else - pack = new Package(options) - - pack.preload() - @preloadedPackages[packageName] = pack - - loadPackages: -> - # Ensure atom exports is already in the require cache so the load time - # of the first package isn't skewed by being the first to require atom - require '../exports/atom' - - disabledPackageNames = new Set(@config.get('core.disabledPackages')) - @config.transact => - for pack in @getAvailablePackages() - @loadAvailablePackage(pack, disabledPackageNames) - return - @initialPackagesLoaded = true - @emitter.emit 'did-load-initial-packages' - - loadPackage: (nameOrPath) -> - if path.basename(nameOrPath)[0].match(/^\./) # primarily to skip .git folder - null - else if pack = @getLoadedPackage(nameOrPath) - pack - else if packagePath = @resolvePackagePath(nameOrPath) - name = path.basename(nameOrPath) - @loadAvailablePackage({name, path: packagePath, isBundled: @isBundledPackagePath(packagePath)}) - else - console.warn "Could not resolve '#{nameOrPath}' to a package path" - null - - loadAvailablePackage: (availablePackage, disabledPackageNames) -> - preloadedPackage = @preloadedPackages[availablePackage.name] - - if disabledPackageNames?.has(availablePackage.name) - if preloadedPackage? - preloadedPackage.deactivate() - delete preloadedPackage[availablePackage.name] - else - loadedPackage = @getLoadedPackage(availablePackage.name) - if loadedPackage? - loadedPackage - else - if preloadedPackage? - if availablePackage.isBundled - preloadedPackage.finishLoading() - @loadedPackages[availablePackage.name] = preloadedPackage - return preloadedPackage - else - preloadedPackage.deactivate() - delete preloadedPackage[availablePackage.name] - - try - metadata = @loadPackageMetadata(availablePackage) ? {} - catch error - @handleMetadataError(error, availablePackage.path) - return null - - unless availablePackage.isBundled - if @isDeprecatedPackage(metadata.name, metadata.version) - console.warn "Could not load #{metadata.name}@#{metadata.version} because it uses deprecated APIs that have been removed." - return null - - options = { - path: availablePackage.path, name: availablePackage.name, metadata, - bundledPackage: availablePackage.isBundled, packageManager: this, - @config, @styleManager, @commandRegistry, @keymapManager, - @notificationManager, @grammarRegistry, @themeManager, @menuManager, - @contextMenuManager, @deserializerManager, @viewRegistry - } - if metadata.theme - pack = new ThemePackage(options) - else - pack = new Package(options) - pack.load() - @loadedPackages[pack.name] = pack - @emitter.emit 'did-load-package', pack - pack - - unloadPackages: -> - @unloadPackage(name) for name in _.keys(@loadedPackages) - null - - unloadPackage: (name) -> - if @isPackageActive(name) - throw new Error("Tried to unload active package '#{name}'") - - if pack = @getLoadedPackage(name) - delete @loadedPackages[pack.name] - @emitter.emit 'did-unload-package', pack - else - throw new Error("No loaded package for name '#{name}'") - - # Activate all the packages that should be activated. - activate: -> - promises = [] - for [activator, types] in @packageActivators - packages = @getLoadedPackagesForTypes(types) - promises = promises.concat(activator.activatePackages(packages)) - Promise.all(promises).then => - @triggerDeferredActivationHooks() - @initialPackagesActivated = true - @emitter.emit 'did-activate-initial-packages' - - # another type of package manager can handle other package types. - # See ThemeManager - registerPackageActivator: (activator, types) -> - @packageActivators.push([activator, types]) - - activatePackages: (packages) -> - promises = [] - @config.transactAsync => - for pack in packages - promise = @activatePackage(pack.name) - promises.push(promise) unless pack.activationShouldBeDeferred() - Promise.all(promises) - @observeDisabledPackages() - @observePackagesWithKeymapsDisabled() - promises - - # Activate a single package by name - activatePackage: (name) -> - if pack = @getActivePackage(name) - Promise.resolve(pack) - else if pack = @loadPackage(name) - @activatingPackages[pack.name] = pack - activationPromise = pack.activate().then => - if @activatingPackages[pack.name]? - delete @activatingPackages[pack.name] - @activePackages[pack.name] = pack - @emitter.emit 'did-activate-package', pack - pack - - unless @deferredActivationHooks? - @triggeredActivationHooks.forEach((hook) => @activationHookEmitter.emit(hook)) - - activationPromise - else - Promise.reject(new Error("Failed to load package '#{name}'")) - - triggerDeferredActivationHooks: -> - return unless @deferredActivationHooks? - @activationHookEmitter.emit(hook) for hook in @deferredActivationHooks - @deferredActivationHooks = null - - triggerActivationHook: (hook) -> - return new Error("Cannot trigger an empty activation hook") unless hook? and _.isString(hook) and hook.length > 0 - @triggeredActivationHooks.add(hook) - if @deferredActivationHooks? - @deferredActivationHooks.push hook - else - @activationHookEmitter.emit(hook) - - onDidTriggerActivationHook: (hook, callback) -> - return unless hook? and _.isString(hook) and hook.length > 0 - @activationHookEmitter.on(hook, callback) - - serialize: -> - for pack in @getActivePackages() - @serializePackage(pack) - @packageStates - - serializePackage: (pack) -> - @setPackageState(pack.name, state) if state = pack.serialize?() - - # Deactivate all packages - deactivatePackages: -> - @config.transact => - @deactivatePackage(pack.name, true) for pack in @getLoadedPackages() - return - @unobserveDisabledPackages() - @unobservePackagesWithKeymapsDisabled() - - # Deactivate the package with the given name - deactivatePackage: (name, suppressSerialization) -> - pack = @getLoadedPackage(name) - @serializePackage(pack) if not suppressSerialization and @isPackageActive(pack.name) - pack.deactivate() - delete @activePackages[pack.name] - delete @activatingPackages[pack.name] - @emitter.emit 'did-deactivate-package', pack - - handleMetadataError: (error, packagePath) -> - metadataPath = path.join(packagePath, 'package.json') - detail = "#{error.message} in #{metadataPath}" - stack = "#{error.stack}\n at #{metadataPath}:1:1" - message = "Failed to load the #{path.basename(packagePath)} package" - @notificationManager.addError(message, {stack, detail, packageName: path.basename(packagePath), dismissable: true}) - - uninstallDirectory: (directory) -> - symlinkPromise = new Promise (resolve) -> - fs.isSymbolicLink directory, (isSymLink) -> resolve(isSymLink) - - dirPromise = new Promise (resolve) -> - fs.isDirectory directory, (isDir) -> resolve(isDir) - - Promise.all([symlinkPromise, dirPromise]).then (values) -> - [isSymLink, isDir] = values - if not isSymLink and isDir - fs.remove directory, -> - - reloadActivePackageStyleSheets: -> - for pack in @getActivePackages() when pack.getType() isnt 'theme' - pack.reloadStylesheets?() - return - - isBundledPackagePath: (packagePath) -> - if @devMode - return false unless @resourcePath.startsWith("#{process.resourcesPath}#{path.sep}") - - @resourcePathWithTrailingSlash ?= "#{@resourcePath}#{path.sep}" - packagePath?.startsWith(@resourcePathWithTrailingSlash) - - loadPackageMetadata: (packagePathOrAvailablePackage, ignoreErrors=false) -> - if typeof packagePathOrAvailablePackage is 'object' - availablePackage = packagePathOrAvailablePackage - packageName = availablePackage.name - packagePath = availablePackage.path - isBundled = availablePackage.isBundled - else - packagePath = packagePathOrAvailablePackage - packageName = path.basename(packagePath) - isBundled = @isBundledPackagePath(packagePath) - - if isBundled - metadata = @packagesCache[packageName]?.metadata - - unless metadata? - if metadataPath = CSON.resolve(path.join(packagePath, 'package')) - try - metadata = CSON.readFileSync(metadataPath) - @normalizePackageMetadata(metadata) - catch error - throw error unless ignoreErrors - - metadata ?= {} - unless typeof metadata.name is 'string' and metadata.name.length > 0 - metadata.name = packageName - - if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string' - metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '') - - metadata - - normalizePackageMetadata: (metadata) -> - unless metadata?._id - normalizePackageData ?= require 'normalize-package-data' - normalizePackageData(metadata) diff --git a/src/package-manager.js b/src/package-manager.js new file mode 100644 index 000000000..792a4e692 --- /dev/null +++ b/src/package-manager.js @@ -0,0 +1,851 @@ +const path = require('path') +let normalizePackageData = null + +const _ = require('underscore-plus') +const {Emitter} = require('event-kit') +const fs = require('fs-plus') +const CSON = require('season') + +const ServiceHub = require('service-hub') +const Package = require('./package') +const ThemePackage = require('./theme-package') +const {isDeprecatedPackage, getDeprecatedPackageMetadata} = require('./deprecated-packages') +const packageJSON = require('../package.json') + +// Extended: Package manager for coordinating the lifecycle of Atom packages. +// +// An instance of this class is always available as the `atom.packages` global. +// +// Packages can be loaded, activated, and deactivated, and unloaded: +// * Loading a package reads and parses the package's metadata and resources +// such as keymaps, menus, stylesheets, etc. +// * Activating a package registers the loaded resources and calls `activate()` +// on the package's main module. +// * Deactivating a package unregisters the package's resources and calls +// `deactivate()` on the package's main module. +// * Unloading a package removes it completely from the package manager. +// +// Packages can be enabled/disabled via the `core.disabledPackages` config +// settings and also by calling `enablePackage()/disablePackage()`. +module.exports = class PackageManager { + constructor (params) { + ({ + config: this.config, styleManager: this.styleManager, notificationManager: this.notificationManager, keymapManager: this.keymapManager, + commandRegistry: this.commandRegistry, grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry + } = params) + + this.emitter = new Emitter() + this.activationHookEmitter = new Emitter() + this.packageDirPaths = [] + this.deferredActivationHooks = [] + this.triggeredActivationHooks = new Set() + this.packagesCache = packageJSON._atomPackages != null ? packageJSON._atomPackages : {} + this.packageDependencies = packageJSON.packageDependencies != null ? packageJSON.packageDependencies : {} + this.initialPackagesLoaded = false + this.initialPackagesActivated = false + this.preloadedPackages = {} + this.loadedPackages = {} + this.activePackages = {} + this.activatingPackages = {} + this.packageStates = {} + this.serviceHub = new ServiceHub() + + this.packageActivators = [] + this.registerPackageActivator(this, ['atom', 'textmate']) + } + + initialize (params) { + this.devMode = params.devMode + this.resourcePath = params.resourcePath + if (params.configDirPath != null && !params.safeMode) { + this.packageDirPaths.push(path.join(params.configDirPath, this.devMode ? 'dev' : '', 'packages')) + } + } + + setContextMenuManager (contextMenuManager) { + this.contextMenuManager = contextMenuManager + } + + setMenuManager (menuManager) { + this.menuManager = menuManager + } + + setThemeManager (themeManager) { + this.themeManager = themeManager + } + + reset () { + this.serviceHub.clear() + this.deactivatePackages() + this.loadedPackages = {} + this.preloadedPackages = {} + this.packageStates = {} + this.packagesCache = packageJSON._atomPackages != null ? packageJSON._atomPackages : {} + this.packageDependencies = packageJSON.packageDependencies != null ? packageJSON.packageDependencies : {} + this.triggeredActivationHooks.clear() + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when all packages have been loaded. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidLoadInitialPackages (callback) { + return this.emitter.on('did-load-initial-packages', callback) + } + + // Public: Invoke the given callback when all packages have been activated. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidActivateInitialPackages (callback) { + return this.emitter.on('did-activate-initial-packages', callback) + } + + // Public: Invoke the given callback when a package is activated. + // + // * `callback` A {Function} to be invoked when a package is activated. + // * `package` The {Package} that was activated. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidActivatePackage (callback) { + return this.emitter.on('did-activate-package', callback) + } + + // Public: Invoke the given callback when a package is deactivated. + // + // * `callback` A {Function} to be invoked when a package is deactivated. + // * `package` The {Package} that was deactivated. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDeactivatePackage (callback) { + return this.emitter.on('did-deactivate-package', callback) + } + + // Public: Invoke the given callback when a package is loaded. + // + // * `callback` A {Function} to be invoked when a package is loaded. + // * `package` The {Package} that was loaded. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidLoadPackage (callback) { + return this.emitter.on('did-load-package', callback) + } + + // Public: Invoke the given callback when a package is unloaded. + // + // * `callback` A {Function} to be invoked when a package is unloaded. + // * `package` The {Package} that was unloaded. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidUnloadPackage (callback) { + return this.emitter.on('did-unload-package', callback) + } + + /* + Section: Package system data + */ + + // Public: Get the path to the apm command. + // + // Uses the value of the `core.apmPath` config setting if it exists. + // + // Return a {String} file path to apm. + getApmPath () { + const configPath = atom.config.get('core.apmPath') + if (configPath || this.apmPath) { + return configPath || this.apmPath + } + + const commandName = process.platform === 'win32' ? 'apm.cmd' : 'apm' + const apmRoot = path.join(process.resourcesPath, 'app', 'apm') + this.apmPath = path.join(apmRoot, 'bin', commandName) + if (!fs.isFileSync(this.apmPath)) { + this.apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName) + } + return this.apmPath + } + + // Public: Get the paths being used to look for packages. + // + // Returns an {Array} of {String} directory paths. + getPackageDirPaths () { + return _.clone(this.packageDirPaths) + } + + /* + Section: General package data + */ + + // Public: Resolve the given package name to a path on disk. + // + // * `name` - The {String} package name. + // + // Return a {String} folder path or undefined if it could not be resolved. + resolvePackagePath (name) { + if (fs.isDirectorySync(name)) { + return name + } + + let packagePath = fs.resolve(...this.packageDirPaths, name) + if (fs.isDirectorySync(packagePath)) { + return packagePath + } + + packagePath = path.join(this.resourcePath, 'node_modules', name) + if (this.hasAtomEngine(packagePath)) { + return packagePath + } + + return null + } + + // Public: Is the package with the given name bundled with Atom? + // + // * `name` - The {String} package name. + // + // Returns a {Boolean}. + isBundledPackage (name) { + return this.getPackageDependencies().hasOwnProperty(name) + } + + isDeprecatedPackage (name, version) { + return isDeprecatedPackage(name, version) + } + + getDeprecatedPackageMetadata (name) { + return getDeprecatedPackageMetadata(name) + } + + /* + Section: Enabling and disabling packages + */ + + // Public: Enable the package with the given name. + // + // * `name` - The {String} package name. + // + // Returns the {Package} that was enabled or null if it isn't loaded. + enablePackage (name) { + const pack = this.loadPackage(name) + if (pack != null) { + pack.enable() + } + return pack + } + + // Public: Disable the package with the given name. + // + // * `name` - The {String} package name. + // + // Returns the {Package} that was disabled or null if it isn't loaded. + disablePackage (name) { + const pack = this.loadPackage(name) + if (!this.isPackageDisabled(name) && pack != null) { + pack.disable() + } + return pack + } + + // Public: Is the package with the given name disabled? + // + // * `name` - The {String} package name. + // + // Returns a {Boolean}. + isPackageDisabled (name) { + return _.include(this.config.get('core.disabledPackages') || [], name) + } + + /* + Section: Accessing active packages + */ + + // Public: Get an {Array} of all the active {Package}s. + getActivePackages () { + return _.values(this.activePackages) + } + + // Public: Get the active {Package} with the given name. + // + // * `name` - The {String} package name. + // + // Returns a {Package} or undefined. + getActivePackage (name) { + return this.activePackages[name] + } + + // Public: Is the {Package} with the given name active? + // + // * `name` - The {String} package name. + // + // Returns a {Boolean}. + isPackageActive (name) { + return (this.getActivePackage(name) != null) + } + + // Public: Returns a {Boolean} indicating whether package activation has occurred. + hasActivatedInitialPackages () { + return this.initialPackagesActivated + } + + /* + Section: Accessing loaded packages + */ + + // Public: Get an {Array} of all the loaded {Package}s + getLoadedPackages () { + return _.values(this.loadedPackages) + } + + // Get packages for a certain package type + // + // * `types` an {Array} of {String}s like ['atom', 'textmate']. + getLoadedPackagesForTypes (types) { + return this.getLoadedPackages().filter(p => types.includes(p.getType())) + } + + // Public: Get the loaded {Package} with the given name. + // + // * `name` - The {String} package name. + // + // Returns a {Package} or undefined. + getLoadedPackage (name) { + return this.loadedPackages[name] + } + + // Public: Is the package with the given name loaded? + // + // * `name` - The {String} package name. + // + // Returns a {Boolean}. + isPackageLoaded (name) { + return this.getLoadedPackage(name) != null + } + + // Public: Returns a {Boolean} indicating whether package loading has occurred. + hasLoadedInitialPackages () { + return this.initialPackagesLoaded + } + + /* + Section: Accessing available packages + */ + + // Public: Returns an {Array} of {String}s of all the available package paths. + getAvailablePackagePaths () { + return this.getAvailablePackages().map(a => a.path) + } + + // Public: Returns an {Array} of {String}s of all the available package names. + getAvailablePackageNames () { + return this.getAvailablePackages().map(a => a.name) + } + + // Public: Returns an {Array} of {String}s of all the available package metadata. + getAvailablePackageMetadata () { + const packages = [] + for (const pack of this.getAvailablePackages()) { + const loadedPackage = this.getLoadedPackage(pack.name) + const metadata = loadedPackage != null ? loadedPackage.metadata : this.loadPackageMetadata(pack, true) + packages.push(metadata) + } + return packages + } + + getAvailablePackages () { + const packages = [] + const packagesByName = new Set() + + for (const packageDirPath of this.packageDirPaths) { + if (fs.isDirectorySync(packageDirPath)) { + for (let packagePath of fs.readdirSync(packageDirPath)) { + packagePath = path.join(packageDirPath, packagePath) + const packageName = path.basename(packagePath) + if (!packageName.startsWith('.') && !packagesByName.has(packageName) && fs.isDirectorySync(packagePath)) { + packages.push({ + name: packageName, + path: packagePath, + isBundled: false + }) + packagesByName.add(packageName) + } + } + } + } + + for (const packageName in this.packageDependencies) { + if (!packagesByName.has(packageName)) { + packages.push({ + name: packageName, + path: path.join(this.resourcePath, 'node_modules', packageName), + isBundled: true + }) + } + } + + return packages.sort((a, b) => a.name.localeCompare(b.name)) + } + + /* + Section: Private + */ + + getPackageState (name) { + return this.packageStates[name] + } + + setPackageState (name, state) { + this.packageStates[name] = state + } + + getPackageDependencies () { + return this.packageDependencies + } + + hasAtomEngine (packagePath) { + const metadata = this.loadPackageMetadata(packagePath, true) + return metadata != null && metadata.engines != null && metadata.engines.atom != null + } + + unobserveDisabledPackages () { + if (this.disabledPackagesSubscription != null) { + this.disabledPackagesSubscription.dispose() + } + this.disabledPackagesSubscription = null + } + + observeDisabledPackages () { + if (this.disabledPackagesSubscription != null) { + return + } + + this.disabledPackagesSubscription = this.config.onDidChange('core.disabledPackages', ({newValue, oldValue}) => { + const packagesToEnable = _.difference(oldValue, newValue) + const packagesToDisable = _.difference(newValue, oldValue) + packagesToDisable.forEach(name => { if (this.getActivePackage(name)) this.deactivatePackage(name) }) + packagesToEnable.forEach(name => this.activatePackage(name)) + return null + }) + } + + unobservePackagesWithKeymapsDisabled () { + if (this.packagesWithKeymapsDisabledSubscription != null) { + this.packagesWithKeymapsDisabledSubscription.dispose() + } + this.packagesWithKeymapsDisabledSubscription = null + } + + observePackagesWithKeymapsDisabled () { + if (this.packagesWithKeymapsDisabledSubscription != null) { + return + } + + const performOnLoadedActivePackages = (packageNames, disabledPackageNames, action) => { + for (const packageName of packageNames) { + if (!disabledPackageNames.has(packageName)) { + var pack = this.getLoadedPackage(packageName) + if (pack != null) { + action(pack) + } + } + } + } + + this.packagesWithKeymapsDisabledSubscription = this.config.onDidChange('core.packagesWithKeymapsDisabled', ({newValue, oldValue}) => { + const keymapsToEnable = _.difference(oldValue, newValue) + const keymapsToDisable = _.difference(newValue, oldValue) + + const disabledPackageNames = new Set(this.config.get('core.disabledPackages')) + performOnLoadedActivePackages(keymapsToDisable, disabledPackageNames, p => p.deactivateKeymaps()) + performOnLoadedActivePackages(keymapsToEnable, disabledPackageNames, p => p.activateKeymaps()) + return null + }) + } + + preloadPackages () { + return Object.entries(this.packagesCache).map(p => this.preloadPackage(p[0], p[1])) + } + + preloadPackage (packageName, pack) { + const metadata = pack.metadata || {} + if (typeof metadata.name !== 'string' || metadata.name.length < 1) { + metadata.name = packageName + } + + if (metadata.repository != null && metadata.repository.type === 'git' && typeof metadata.repository.url === 'string') { + metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '') + } + + const options = { + path: pack.rootDirPath, + name: packageName, + preloadedPackage: true, + bundledPackage: true, + metadata, + packageManager: this, + config: this.config, + styleManager: this.styleManager, + commandRegistry: this.commandRegistry, + keymapManager: this.keymapManager, + notificationManager: this.notificationManager, + grammarRegistry: this.grammarRegistry, + themeManager: this.themeManager, + menuManager: this.menuManager, + contextMenuManager: this.contextMenuManager, + deserializerManager: this.deserializerManager, + viewRegistry: this.viewRegistry + } + + pack = metadata.theme ? new ThemePackage(options) : new Package(options) + pack.preload() + this.preloadedPackages[packageName] = pack + return pack + } + + loadPackages () { + // Ensure atom exports is already in the require cache so the load time + // of the first package isn't skewed by being the first to require atom + require('../exports/atom') + + const disabledPackageNames = new Set(this.config.get('core.disabledPackages')) + this.config.transact(() => { + for (const pack of this.getAvailablePackages()) { + this.loadAvailablePackage(pack, disabledPackageNames) + } + }) + this.initialPackagesLoaded = true + this.emitter.emit('did-load-initial-packages') + } + + loadPackage (nameOrPath) { + if (path.basename(nameOrPath)[0].match(/^\./)) { // primarily to skip .git folder + return null + } + + const pack = this.getLoadedPackage(nameOrPath) + if (pack) { + return pack + } + + const packagePath = this.resolvePackagePath(nameOrPath) + if (packagePath) { + const name = path.basename(nameOrPath) + return this.loadAvailablePackage({name, path: packagePath, isBundled: this.isBundledPackagePath(packagePath)}) + } + + console.warn(`Could not resolve '${nameOrPath}' to a package path`) + return null + } + + loadAvailablePackage (availablePackage, disabledPackageNames) { + const preloadedPackage = this.preloadedPackages[availablePackage.name] + + if (disabledPackageNames != null && disabledPackageNames.has(availablePackage.name)) { + if (preloadedPackage != null) { + preloadedPackage.deactivate() + delete preloadedPackage[availablePackage.name] + } + return null + } + + const loadedPackage = this.getLoadedPackage(availablePackage.name) + if (loadedPackage != null) { + return loadedPackage + } + + if (preloadedPackage != null) { + if (availablePackage.isBundled) { + preloadedPackage.finishLoading() + this.loadedPackages[availablePackage.name] = preloadedPackage + return preloadedPackage + } else { + preloadedPackage.deactivate() + delete preloadedPackage[availablePackage.name] + } + } + + let metadata + try { + metadata = this.loadPackageMetadata(availablePackage) || {} + } catch (error) { + this.handleMetadataError(error, availablePackage.path) + return null + } + + if (!availablePackage.isBundled && this.isDeprecatedPackage(metadata.name, metadata.version)) { + console.warn(`Could not load ${metadata.name}@${metadata.version} because it uses deprecated APIs that have been removed.`) + return null + } + + const options = { + path: availablePackage.path, + name: availablePackage.name, + metadata, + bundledPackage: availablePackage.isBundled, + packageManager: this, + config: this.config, + styleManager: this.styleManager, + commandRegistry: this.commandRegistry, + keymapManager: this.keymapManager, + notificationManager: this.notificationManager, + grammarRegistry: this.grammarRegistry, + themeManager: this.themeManager, + menuManager: this.menuManager, + contextMenuManager: this.contextMenuManager, + deserializerManager: this.deserializerManager, + viewRegistry: this.viewRegistry + } + + const pack = metadata.theme ? new ThemePackage(options) : new Package(options) + pack.load() + this.loadedPackages[pack.name] = pack + this.emitter.emit('did-load-package', pack) + return pack + } + + unloadPackages () { + _.keys(this.loadedPackages).forEach(name => this.unloadPackage(name)) + } + + unloadPackage (name) { + if (this.isPackageActive(name)) { + throw new Error(`Tried to unload active package '${name}'`) + } + + const pack = this.getLoadedPackage(name) + if (pack) { + delete this.loadedPackages[pack.name] + this.emitter.emit('did-unload-package', pack) + } else { + throw new Error(`No loaded package for name '${name}'`) + } + } + + // Activate all the packages that should be activated. + activate () { + let promises = [] + for (let [activator, types] of this.packageActivators) { + const packages = this.getLoadedPackagesForTypes(types) + promises = promises.concat(activator.activatePackages(packages)) + } + return Promise.all(promises).then(() => { + this.triggerDeferredActivationHooks() + this.initialPackagesActivated = true + this.emitter.emit('did-activate-initial-packages') + }) + } + + // another type of package manager can handle other package types. + // See ThemeManager + registerPackageActivator (activator, types) { + this.packageActivators.push([activator, types]) + } + + activatePackages (packages) { + const promises = [] + this.config.transactAsync(() => { + for (const pack of packages) { + const promise = this.activatePackage(pack.name) + if (!pack.activationShouldBeDeferred()) { + promises.push(promise) + } + } + return Promise.all(promises) + }) + this.observeDisabledPackages() + this.observePackagesWithKeymapsDisabled() + return promises + } + + // Activate a single package by name + activatePackage (name) { + let pack = this.getActivePackage(name) + if (pack) { + return Promise.resolve(pack) + } + + pack = this.loadPackage(name) + if (!pack) { + return Promise.reject(new Error(`Failed to load package '${name}'`)) + } + + this.activatingPackages[pack.name] = pack + const activationPromise = pack.activate().then(() => { + if (this.activatingPackages[pack.name] != null) { + delete this.activatingPackages[pack.name] + this.activePackages[pack.name] = pack + this.emitter.emit('did-activate-package', pack) + } + return pack + }) + + if (this.deferredActivationHooks == null) { + this.triggeredActivationHooks.forEach(hook => this.activationHookEmitter.emit(hook)) + } + + return activationPromise + } + + triggerDeferredActivationHooks () { + if (this.deferredActivationHooks == null) { + return + } + + for (const hook of this.deferredActivationHooks) { + this.activationHookEmitter.emit(hook) + } + + this.deferredActivationHooks = null + } + + triggerActivationHook (hook) { + if (hook == null || !_.isString(hook) || hook.length <= 0) { + return new Error('Cannot trigger an empty activation hook') + } + + this.triggeredActivationHooks.add(hook) + if (this.deferredActivationHooks != null) { + this.deferredActivationHooks.push(hook) + } else { + this.activationHookEmitter.emit(hook) + } + } + + onDidTriggerActivationHook (hook, callback) { + if (hook == null || !_.isString(hook) || hook.length <= 0) { + return + } + return this.activationHookEmitter.on(hook, callback) + } + + serialize () { + for (const pack of this.getActivePackages()) { + this.serializePackage(pack) + } + return this.packageStates + } + + serializePackage (pack) { + if (typeof pack.serialize === 'function') { + this.setPackageState(pack.name, pack.serialize()) + } + } + + // Deactivate all packages + deactivatePackages () { + this.config.transact(() => { + this.getLoadedPackages().forEach(pack => this.deactivatePackage(pack.name, true)) + }) + this.unobserveDisabledPackages() + this.unobservePackagesWithKeymapsDisabled() + } + + // Deactivate the package with the given name + deactivatePackage (name, suppressSerialization) { + const pack = this.getLoadedPackage(name) + if (!suppressSerialization && this.isPackageActive(pack.name)) { + this.serializePackage(pack) + } + pack.deactivate() + delete this.activePackages[pack.name] + delete this.activatingPackages[pack.name] + this.emitter.emit('did-deactivate-package', pack) + } + + handleMetadataError (error, packagePath) { + const metadataPath = path.join(packagePath, 'package.json') + const detail = `${error.message} in ${metadataPath}` + const stack = `${error.stack}\n at ${metadataPath}:1:1` + const message = `Failed to load the ${path.basename(packagePath)} package` + this.notificationManager.addError(message, {stack, detail, packageName: path.basename(packagePath), dismissable: true}) + } + + uninstallDirectory (directory) { + const symlinkPromise = new Promise(resolve => fs.isSymbolicLink(directory, isSymLink => resolve(isSymLink))) + const dirPromise = new Promise(resolve => fs.isDirectory(directory, isDir => resolve(isDir))) + + return Promise.all([symlinkPromise, dirPromise]).then(values => { + const [isSymLink, isDir] = values + if (!isSymLink && isDir) { + return fs.remove(directory, function () {}) + } + }) + } + + reloadActivePackageStyleSheets () { + for (const pack of this.getActivePackages()) { + if (pack.getType() !== 'theme' && typeof pack.reloadStylesheets === 'function') { + pack.reloadStylesheets() + } + } + } + + isBundledPackagePath (packagePath) { + if (this.devMode && !this.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`)) { + return false + } + + if (this.resourcePathWithTrailingSlash == null) { + this.resourcePathWithTrailingSlash = `${this.resourcePath}${path.sep}` + } + + return packagePath != null && packagePath.startsWith(this.resourcePathWithTrailingSlash) + } + + loadPackageMetadata (packagePathOrAvailablePackage, ignoreErrors = false) { + let isBundled, packageName, packagePath + if (typeof packagePathOrAvailablePackage === 'object') { + const availablePackage = packagePathOrAvailablePackage + packageName = availablePackage.name + packagePath = availablePackage.path + isBundled = availablePackage.isBundled + } else { + packagePath = packagePathOrAvailablePackage + packageName = path.basename(packagePath) + isBundled = this.isBundledPackagePath(packagePath) + } + + let metadata + if (isBundled && this.packagesCache[packageName] != null) { + metadata = this.packagesCache[packageName].metadata + } + + if (metadata == null) { + const metadataPath = CSON.resolve(path.join(packagePath, 'package')) + if (metadataPath) { + try { + metadata = CSON.readFileSync(metadataPath) + this.normalizePackageMetadata(metadata) + } catch (error) { + if (!ignoreErrors) { throw error } + } + } + } + + if (metadata == null) { + metadata = {} + } + + if (typeof metadata.name !== 'string' || metadata.name.length <= 0) { + metadata.name = packageName + } + + if (metadata.repository && metadata.repository.type === 'git' && typeof metadata.repository.url === 'string') { + metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '') + } + + return metadata + } + + normalizePackageMetadata (metadata) { + if (metadata != null) { + normalizePackageData = normalizePackageData || require('normalize-package-data') + normalizePackageData(metadata) + } + } +}