mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
1035 lines
29 KiB
JavaScript
1035 lines
29 KiB
JavaScript
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 ModuleCache = require('./module-cache');
|
|
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,
|
|
uriHandlerRegistry: this.uriHandlerRegistry
|
|
} = 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.deprecatedPackages = packageJSON._deprecatedPackages || {};
|
|
this.deprecatedPackageRanges = {};
|
|
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) {
|
|
if (this.devMode) {
|
|
this.packageDirPaths.push(
|
|
path.join(params.configDirPath, 'dev', 'packages')
|
|
);
|
|
this.packageDirPaths.push(path.join(this.resourcePath, 'packages'));
|
|
}
|
|
this.packageDirPaths.push(path.join(params.configDirPath, 'packages'));
|
|
}
|
|
}
|
|
|
|
setContextMenuManager(contextMenuManager) {
|
|
this.contextMenuManager = contextMenuManager;
|
|
}
|
|
|
|
setMenuManager(menuManager) {
|
|
this.menuManager = menuManager;
|
|
}
|
|
|
|
setThemeManager(themeManager) {
|
|
this.themeManager = themeManager;
|
|
}
|
|
|
|
async reset() {
|
|
this.serviceHub.clear();
|
|
await 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();
|
|
this.activatePromise = null;
|
|
}
|
|
|
|
/*
|
|
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);
|
|
}
|
|
|
|
getActivatePromise() {
|
|
if (this.activatePromise) {
|
|
return this.activatePromise;
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
const metadata = this.deprecatedPackages[name];
|
|
if (!metadata) return false;
|
|
if (!metadata.version) return true;
|
|
|
|
let range = this.deprecatedPackageRanges[metadata.version];
|
|
if (!range) {
|
|
try {
|
|
range = new ModuleCache.Range(metadata.version);
|
|
} catch (error) {
|
|
range = NullVersionRange;
|
|
}
|
|
this.deprecatedPackageRanges[metadata.version] = range;
|
|
}
|
|
return range.test(version);
|
|
}
|
|
|
|
getDeprecatedPackageMetadata(name) {
|
|
const metadata = this.deprecatedPackages[name];
|
|
if (metadata) Object.freeze(metadata);
|
|
return metadata;
|
|
}
|
|
|
|
/*
|
|
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)) {
|
|
// checks for directories.
|
|
// dirent is faster, but for checking symbolic link we need stat.
|
|
const packageNames = fs
|
|
.readdirSync(packageDirPath, { withFileTypes: true })
|
|
.filter(
|
|
dirent =>
|
|
dirent.isDirectory() ||
|
|
(dirent.isSymbolicLink() &&
|
|
fs.isDirectorySync(path.join(packageDirPath, dirent.name)))
|
|
)
|
|
.map(dirent => dirent.name);
|
|
|
|
for (const packageName of packageNames) {
|
|
if (
|
|
!packageName.startsWith('.') &&
|
|
!packagesByName.has(packageName)
|
|
) {
|
|
const packagePath = path.join(packageDirPath, packageName);
|
|
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() {
|
|
const result = [];
|
|
for (const packageName in this.packagesCache) {
|
|
result.push(
|
|
this.preloadPackage(packageName, this.packagesCache[packageName])
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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));
|
|
}
|
|
this.activatePromise = Promise.all(promises).then(() => {
|
|
this.triggerDeferredActivationHooks();
|
|
this.initialPackagesActivated = true;
|
|
this.emitter.emit('did-activate-initial-packages');
|
|
this.activatePromise = null;
|
|
});
|
|
return this.activatePromise;
|
|
}
|
|
|
|
registerURIHandlerForPackage(packageName, handler) {
|
|
return this.uriHandlerRegistry.registerHostHandler(packageName, handler);
|
|
}
|
|
|
|
// 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
|
|
async deactivatePackages() {
|
|
await this.config.transactAsync(() =>
|
|
Promise.all(
|
|
this.getLoadedPackages().map(pack =>
|
|
this.deactivatePackage(pack.name, true)
|
|
)
|
|
)
|
|
);
|
|
this.unobserveDisabledPackages();
|
|
this.unobservePackagesWithKeymapsDisabled();
|
|
}
|
|
|
|
// Deactivate the package with the given name
|
|
async deactivatePackage(name, suppressSerialization) {
|
|
const pack = this.getLoadedPackage(name);
|
|
if (pack == null) {
|
|
return;
|
|
}
|
|
|
|
if (!suppressSerialization && this.isPackageActive(pack.name)) {
|
|
this.serializePackage(pack);
|
|
}
|
|
|
|
const deactivationResult = pack.deactivate();
|
|
if (deactivationResult && typeof deactivationResult.then === 'function') {
|
|
await deactivationResult;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
};
|
|
|
|
const NullVersionRange = {
|
|
test() {
|
|
return false;
|
|
}
|
|
};
|