Atom/src/package.js

1382 lines
41 KiB
JavaScript

const path = require('path');
const async = require('async');
const CSON = require('season');
const fs = require('fs-plus');
const { Emitter, CompositeDisposable } = require('event-kit');
const dedent = require('dedent');
const CompileCache = require('./compile-cache');
const ModuleCache = require('./module-cache');
const BufferedProcess = require('./buffered-process');
// Extended: Loads and activates a package's main module and resources such as
// stylesheets, keymaps, grammar, editor properties, and menus.
module.exports = class Package {
/*
Section: Construction
*/
constructor(params) {
this.config = params.config;
this.packageManager = params.packageManager;
this.styleManager = params.styleManager;
this.commandRegistry = params.commandRegistry;
this.keymapManager = params.keymapManager;
this.notificationManager = params.notificationManager;
this.grammarRegistry = params.grammarRegistry;
this.themeManager = params.themeManager;
this.menuManager = params.menuManager;
this.contextMenuManager = params.contextMenuManager;
this.deserializerManager = params.deserializerManager;
this.viewRegistry = params.viewRegistry;
this.emitter = new Emitter();
this.mainModule = null;
this.path = params.path;
this.preloadedPackage = params.preloadedPackage;
this.metadata =
params.metadata || this.packageManager.loadPackageMetadata(this.path);
this.bundledPackage =
params.bundledPackage != null
? params.bundledPackage
: this.packageManager.isBundledPackagePath(this.path);
this.name =
(this.metadata && this.metadata.name) ||
params.name ||
path.basename(this.path);
this.reset();
}
/*
Section: Event Subscription
*/
// Essential: Invoke the given callback when all packages have been activated.
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDeactivate(callback) {
return this.emitter.on('did-deactivate', callback);
}
/*
Section: Instance Methods
*/
enable() {
return this.config.removeAtKeyPath('core.disabledPackages', this.name);
}
disable() {
return this.config.pushAtKeyPath('core.disabledPackages', this.name);
}
isTheme() {
return this.metadata && this.metadata.theme;
}
measure(key, fn) {
const startTime = Date.now();
const value = fn();
this[key] = Date.now() - startTime;
return value;
}
getType() {
return 'atom';
}
getStyleSheetPriority() {
return 0;
}
preload() {
this.loadKeymaps();
this.loadMenus();
this.registerDeserializerMethods();
this.activateCoreStartupServices();
this.registerURIHandler();
this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata();
this.requireMainModule();
this.settingsPromise = this.loadSettings();
this.activationDisposables = new CompositeDisposable();
this.activateKeymaps();
this.activateMenus();
for (let settings of this.settings) {
settings.activate(this.config);
}
this.settingsActivated = true;
}
finishLoading() {
this.measure('loadTime', () => {
this.path = path.join(this.packageManager.resourcePath, this.path);
ModuleCache.add(this.path, this.metadata);
this.loadStylesheets();
// Unfortunately some packages are accessing `@mainModulePath`, so we need
// to compute that variable eagerly also for preloaded packages.
this.getMainModulePath();
});
}
load() {
this.measure('loadTime', () => {
try {
ModuleCache.add(this.path, this.metadata);
this.loadKeymaps();
this.loadMenus();
this.loadStylesheets();
this.registerDeserializerMethods();
this.activateCoreStartupServices();
this.registerURIHandler();
this.registerTranspilerConfig();
this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata();
this.settingsPromise = this.loadSettings();
if (this.shouldRequireMainModuleOnLoad() && this.mainModule == null) {
this.requireMainModule();
}
} catch (error) {
this.handleError(`Failed to load the ${this.name} package`, error);
}
});
return this;
}
unload() {
this.unregisterTranspilerConfig();
}
shouldRequireMainModuleOnLoad() {
return !(
this.metadata.deserializers ||
this.metadata.viewProviders ||
this.metadata.configSchema ||
this.activationShouldBeDeferred() ||
localStorage.getItem(this.getCanDeferMainModuleRequireStorageKey()) ===
'true'
);
}
reset() {
this.stylesheets = [];
this.keymaps = [];
this.menus = [];
this.grammars = [];
this.settings = [];
this.mainInitialized = false;
this.mainActivated = false;
this.deserialized = false;
}
initializeIfNeeded() {
if (this.mainInitialized) return;
this.measure('initializeTime', () => {
try {
// The main module's `initialize()` method is guaranteed to be called
// before its `activate()`. This gives you a chance to handle the
// serialized package state before the package's derserializers and view
// providers are used.
if (!this.mainModule) this.requireMainModule();
if (typeof this.mainModule.initialize === 'function') {
this.mainModule.initialize(
this.packageManager.getPackageState(this.name) || {}
);
}
this.mainInitialized = true;
} catch (error) {
this.handleError(
`Failed to initialize the ${this.name} package`,
error
);
}
});
}
activate() {
if (!this.grammarsPromise) this.grammarsPromise = this.loadGrammars();
if (!this.activationPromise) {
this.activationPromise = new Promise((resolve, reject) => {
this.resolveActivationPromise = resolve;
this.measure('activateTime', () => {
try {
this.activateResources();
if (this.activationShouldBeDeferred()) {
return this.subscribeToDeferredActivation();
} else {
return this.activateNow();
}
} catch (error) {
return this.handleError(
`Failed to activate the ${this.name} package`,
error
);
}
});
});
}
return Promise.all([
this.grammarsPromise,
this.settingsPromise,
this.activationPromise
]);
}
activateNow() {
try {
if (!this.mainModule) this.requireMainModule();
this.configSchemaRegisteredOnActivate = this.registerConfigSchemaFromMainModule();
this.registerViewProviders();
this.activateStylesheets();
if (this.mainModule && !this.mainActivated) {
this.initializeIfNeeded();
if (typeof this.mainModule.activateConfig === 'function') {
this.mainModule.activateConfig();
}
if (typeof this.mainModule.activate === 'function') {
this.mainModule.activate(
this.packageManager.getPackageState(this.name) || {}
);
}
this.mainActivated = true;
this.activateServices();
}
if (this.activationCommandSubscriptions)
this.activationCommandSubscriptions.dispose();
if (this.activationHookSubscriptions)
this.activationHookSubscriptions.dispose();
if (this.workspaceOpenerSubscriptions)
this.workspaceOpenerSubscriptions.dispose();
} catch (error) {
this.handleError(`Failed to activate the ${this.name} package`, error);
}
if (typeof this.resolveActivationPromise === 'function')
this.resolveActivationPromise();
}
registerConfigSchemaFromMetadata() {
const configSchema = this.metadata.configSchema;
if (configSchema) {
this.config.setSchema(this.name, {
type: 'object',
properties: configSchema
});
return true;
} else {
return false;
}
}
registerConfigSchemaFromMainModule() {
if (this.mainModule && !this.configSchemaRegisteredOnLoad) {
if (typeof this.mainModule.config === 'object') {
this.config.setSchema(this.name, {
type: 'object',
properties: this.mainModule.config
});
return true;
}
}
return false;
}
// TODO: Remove. Settings view calls this method currently.
activateConfig() {
if (this.configSchemaRegisteredOnLoad) return;
this.requireMainModule();
this.registerConfigSchemaFromMainModule();
}
activateStylesheets() {
if (this.stylesheetsActivated) return;
this.stylesheetDisposables = new CompositeDisposable();
const priority = this.getStyleSheetPriority();
for (let [sourcePath, source] of this.stylesheets) {
const match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./);
let context;
if (match) {
context = match[1];
} else if (this.metadata.theme === 'syntax') {
context = 'atom-text-editor';
}
this.stylesheetDisposables.add(
this.styleManager.addStyleSheet(source, {
sourcePath,
priority,
context,
skipDeprecatedSelectorsTransformation: this.bundledPackage
})
);
}
this.stylesheetsActivated = true;
}
activateResources() {
if (!this.activationDisposables)
this.activationDisposables = new CompositeDisposable();
const packagesWithKeymapsDisabled = this.config.get(
'core.packagesWithKeymapsDisabled'
);
if (
packagesWithKeymapsDisabled &&
packagesWithKeymapsDisabled.includes(this.name)
) {
this.deactivateKeymaps();
} else if (!this.keymapActivated) {
this.activateKeymaps();
}
if (!this.menusActivated) {
this.activateMenus();
}
if (!this.grammarsActivated) {
for (let grammar of this.grammars) {
grammar.activate();
}
this.grammarsActivated = true;
}
if (!this.settingsActivated) {
for (let settings of this.settings) {
settings.activate(this.config);
}
this.settingsActivated = true;
}
}
activateKeymaps() {
if (this.keymapActivated) return;
this.keymapDisposables = new CompositeDisposable();
const validateSelectors = !this.preloadedPackage;
for (let [keymapPath, map] of this.keymaps) {
this.keymapDisposables.add(
this.keymapManager.add(keymapPath, map, 0, validateSelectors)
);
}
this.menuManager.update();
this.keymapActivated = true;
}
deactivateKeymaps() {
if (!this.keymapActivated) return;
if (this.keymapDisposables) {
this.keymapDisposables.dispose();
}
this.menuManager.update();
this.keymapActivated = false;
}
hasKeymaps() {
for (let [, map] of this.keymaps) {
if (map.length > 0) return true;
}
return false;
}
activateMenus() {
const validateSelectors = !this.preloadedPackage;
for (const [menuPath, map] of this.menus) {
if (map['context-menu']) {
try {
const itemsBySelector = map['context-menu'];
this.activationDisposables.add(
this.contextMenuManager.add(itemsBySelector, validateSelectors)
);
} catch (error) {
if (error.code === 'EBADSELECTOR') {
error.message += ` in ${menuPath}`;
error.stack += `\n at ${menuPath}:1:1`;
}
throw error;
}
}
}
for (const [, map] of this.menus) {
if (map.menu)
this.activationDisposables.add(this.menuManager.add(map.menu));
}
this.menusActivated = true;
}
activateServices() {
let methodName, version, versions;
for (var name in this.metadata.providedServices) {
({ versions } = this.metadata.providedServices[name]);
const servicesByVersion = {};
for (version in versions) {
methodName = versions[version];
if (typeof this.mainModule[methodName] === 'function') {
servicesByVersion[version] = this.mainModule[methodName]();
}
}
this.activationDisposables.add(
this.packageManager.serviceHub.provide(name, servicesByVersion)
);
}
for (name in this.metadata.consumedServices) {
({ versions } = this.metadata.consumedServices[name]);
for (version in versions) {
methodName = versions[version];
if (typeof this.mainModule[methodName] === 'function') {
this.activationDisposables.add(
this.packageManager.serviceHub.consume(
name,
version,
this.mainModule[methodName].bind(this.mainModule)
)
);
}
}
}
}
registerURIHandler() {
const handlerConfig = this.getURIHandler();
const methodName = handlerConfig && handlerConfig.method;
if (methodName) {
this.uriHandlerSubscription = this.packageManager.registerURIHandlerForPackage(
this.name,
(...args) => this.handleURI(methodName, args)
);
}
}
unregisterURIHandler() {
if (this.uriHandlerSubscription) this.uriHandlerSubscription.dispose();
}
handleURI(methodName, args) {
this.activate().then(() => {
if (this.mainModule[methodName])
this.mainModule[methodName].apply(this.mainModule, args);
});
if (!this.mainActivated) this.activateNow();
}
registerTranspilerConfig() {
if (this.metadata.atomTranspilers) {
CompileCache.addTranspilerConfigForPath(
this.path,
this.name,
this.metadata,
this.metadata.atomTranspilers
);
}
}
unregisterTranspilerConfig() {
if (this.metadata.atomTranspilers) {
CompileCache.removeTranspilerConfigForPath(this.path);
}
}
loadKeymaps() {
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
this.keymaps = [];
for (const keymapPath in this.packageManager.packagesCache[this.name]
.keymaps) {
const keymapObject = this.packageManager.packagesCache[this.name]
.keymaps[keymapPath];
this.keymaps.push([`core:${keymapPath}`, keymapObject]);
}
} else {
this.keymaps = this.getKeymapPaths().map(keymapPath => [
keymapPath,
CSON.readFileSync(keymapPath, { allowDuplicateKeys: false }) || {}
]);
}
}
loadMenus() {
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
this.menus = [];
for (const menuPath in this.packageManager.packagesCache[this.name]
.menus) {
const menuObject = this.packageManager.packagesCache[this.name].menus[
menuPath
];
this.menus.push([`core:${menuPath}`, menuObject]);
}
} else {
this.menus = this.getMenuPaths().map(menuPath => [
menuPath,
CSON.readFileSync(menuPath) || {}
]);
}
}
getKeymapPaths() {
const keymapsDirPath = path.join(this.path, 'keymaps');
if (this.metadata.keymaps) {
return this.metadata.keymaps.map(name =>
fs.resolve(keymapsDirPath, name, ['json', 'cson', ''])
);
} else {
return fs.listSync(keymapsDirPath, ['cson', 'json']);
}
}
getMenuPaths() {
const menusDirPath = path.join(this.path, 'menus');
if (this.metadata.menus) {
return this.metadata.menus.map(name =>
fs.resolve(menusDirPath, name, ['json', 'cson', ''])
);
} else {
return fs.listSync(menusDirPath, ['cson', 'json']);
}
}
loadStylesheets() {
this.stylesheets = this.getStylesheetPaths().map(stylesheetPath => [
stylesheetPath,
this.themeManager.loadStylesheet(stylesheetPath, true)
]);
}
registerDeserializerMethods() {
if (this.metadata.deserializers) {
Object.keys(this.metadata.deserializers).forEach(deserializerName => {
const methodName = this.metadata.deserializers[deserializerName];
this.deserializerManager.add({
name: deserializerName,
deserialize: (state, atomEnvironment) => {
this.registerViewProviders();
this.requireMainModule();
this.initializeIfNeeded();
if (atomEnvironment.packages.hasActivatedInitialPackages()) {
// Only explicitly activate the package if initial packages
// have finished activating. This is because deserialization
// generally occurs at Atom startup, which happens before the
// workspace element is added to the DOM and is inconsistent with
// with when initial package activation occurs. Triggering activation
// immediately may cause problems with packages that expect to
// always have access to the workspace element.
// Otherwise, we just set the deserialized flag and package-manager
// will activate this package as normal during initial package activation.
this.activateNow();
}
this.deserialized = true;
return this.mainModule[methodName](state, atomEnvironment);
}
});
});
}
}
activateCoreStartupServices() {
const directoryProviderService =
this.metadata.providedServices &&
this.metadata.providedServices['atom.directory-provider'];
if (directoryProviderService) {
this.requireMainModule();
const servicesByVersion = {};
for (let version in directoryProviderService.versions) {
const methodName = directoryProviderService.versions[version];
if (typeof this.mainModule[methodName] === 'function') {
servicesByVersion[version] = this.mainModule[methodName]();
}
}
this.packageManager.serviceHub.provide(
'atom.directory-provider',
servicesByVersion
);
}
}
registerViewProviders() {
if (this.metadata.viewProviders && !this.registeredViewProviders) {
this.requireMainModule();
this.metadata.viewProviders.forEach(methodName => {
this.viewRegistry.addViewProvider(model => {
this.initializeIfNeeded();
return this.mainModule[methodName](model);
});
});
this.registeredViewProviders = true;
}
}
getStylesheetsPath() {
return path.join(this.path, 'styles');
}
getStylesheetPaths() {
if (
this.bundledPackage &&
this.packageManager.packagesCache[this.name] &&
this.packageManager.packagesCache[this.name].styleSheetPaths
) {
const { styleSheetPaths } = this.packageManager.packagesCache[this.name];
return styleSheetPaths.map(styleSheetPath =>
path.join(this.path, styleSheetPath)
);
} else {
let indexStylesheet;
const stylesheetDirPath = this.getStylesheetsPath();
if (this.metadata.mainStyleSheet) {
return [fs.resolve(this.path, this.metadata.mainStyleSheet)];
} else if (this.metadata.styleSheets) {
return this.metadata.styleSheets.map(name =>
fs.resolve(stylesheetDirPath, name, ['css', 'less', ''])
);
} else if (
(indexStylesheet = fs.resolve(this.path, 'index', ['css', 'less']))
) {
return [indexStylesheet];
} else {
return fs.listSync(stylesheetDirPath, ['css', 'less']);
}
}
}
loadGrammarsSync() {
if (this.grammarsLoaded) return;
let grammarPaths;
if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
({ grammarPaths } = this.packageManager.packagesCache[this.name]);
} else {
grammarPaths = fs.listSync(path.join(this.path, 'grammars'), [
'json',
'cson'
]);
}
for (let grammarPath of grammarPaths) {
if (
this.preloadedPackage &&
this.packageManager.packagesCache[this.name]
) {
grammarPath = path.resolve(
this.packageManager.resourcePath,
grammarPath
);
}
try {
const grammar = this.grammarRegistry.readGrammarSync(grammarPath);
grammar.packageName = this.name;
grammar.bundledPackage = this.bundledPackage;
this.grammars.push(grammar);
grammar.activate();
} catch (error) {
console.warn(
`Failed to load grammar: ${grammarPath}`,
error.stack || error
);
}
}
this.grammarsLoaded = true;
this.grammarsActivated = true;
}
loadGrammars() {
if (this.grammarsLoaded) return Promise.resolve();
const loadGrammar = (grammarPath, callback) => {
if (this.preloadedPackage) {
grammarPath = path.resolve(
this.packageManager.resourcePath,
grammarPath
);
}
return this.grammarRegistry.readGrammar(grammarPath, (error, grammar) => {
if (error) {
const detail = `${error.message} in ${grammarPath}`;
const stack = `${error.stack}\n at ${grammarPath}:1:1`;
this.notificationManager.addFatalError(
`Failed to load a ${this.name} package grammar`,
{ stack, detail, packageName: this.name, dismissable: true }
);
} else {
grammar.packageName = this.name;
grammar.bundledPackage = this.bundledPackage;
this.grammars.push(grammar);
if (this.grammarsActivated) grammar.activate();
}
return callback();
});
};
return new Promise(resolve => {
if (
this.preloadedPackage &&
this.packageManager.packagesCache[this.name]
) {
const { grammarPaths } = this.packageManager.packagesCache[this.name];
return async.each(grammarPaths, loadGrammar, () => resolve());
} else {
const grammarsDirPath = path.join(this.path, 'grammars');
fs.exists(grammarsDirPath, grammarsDirExists => {
if (!grammarsDirExists) return resolve();
fs.list(grammarsDirPath, ['json', 'cson'], (error, grammarPaths) => {
if (error || !grammarPaths) return resolve();
async.each(grammarPaths, loadGrammar, () => resolve());
});
});
}
});
}
loadSettings() {
this.settings = [];
const loadSettingsFile = (settingsPath, callback) => {
return SettingsFile.load(settingsPath, (error, settingsFile) => {
if (error) {
const detail = `${error.message} in ${settingsPath}`;
const stack = `${error.stack}\n at ${settingsPath}:1:1`;
this.notificationManager.addFatalError(
`Failed to load the ${this.name} package settings`,
{ stack, detail, packageName: this.name, dismissable: true }
);
} else {
this.settings.push(settingsFile);
if (this.settingsActivated) settingsFile.activate(this.config);
}
return callback();
});
};
if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
for (let settingsPath in this.packageManager.packagesCache[this.name]
.settings) {
const properties = this.packageManager.packagesCache[this.name]
.settings[settingsPath];
const settingsFile = new SettingsFile(
`core:${settingsPath}`,
properties || {}
);
this.settings.push(settingsFile);
if (this.settingsActivated) settingsFile.activate(this.config);
}
} else {
return new Promise(resolve => {
const settingsDirPath = path.join(this.path, 'settings');
fs.exists(settingsDirPath, settingsDirExists => {
if (!settingsDirExists) return resolve();
fs.list(settingsDirPath, ['json', 'cson'], (error, settingsPaths) => {
if (error || !settingsPaths) return resolve();
async.each(settingsPaths, loadSettingsFile, () => resolve());
});
});
});
}
}
serialize() {
if (this.mainActivated) {
if (typeof this.mainModule.serialize === 'function') {
try {
return this.mainModule.serialize();
} catch (error) {
console.error(
`Error serializing package '${this.name}'`,
error.stack
);
}
}
}
}
async deactivate() {
this.activationPromise = null;
this.resolveActivationPromise = null;
if (this.activationCommandSubscriptions)
this.activationCommandSubscriptions.dispose();
if (this.activationHookSubscriptions)
this.activationHookSubscriptions.dispose();
this.configSchemaRegisteredOnActivate = false;
this.unregisterURIHandler();
this.deactivateResources();
this.deactivateKeymaps();
if (!this.mainActivated) {
this.emitter.emit('did-deactivate');
return;
}
if (typeof this.mainModule.deactivate === 'function') {
try {
const deactivationResult = this.mainModule.deactivate();
if (
deactivationResult &&
typeof deactivationResult.then === 'function'
) {
await deactivationResult;
}
} catch (error) {
console.error(`Error deactivating package '${this.name}'`, error.stack);
}
}
if (typeof this.mainModule.deactivateConfig === 'function') {
try {
await this.mainModule.deactivateConfig();
} catch (error) {
console.error(`Error deactivating package '${this.name}'`, error.stack);
}
}
this.mainActivated = false;
this.mainInitialized = false;
this.emitter.emit('did-deactivate');
}
deactivateResources() {
for (let grammar of this.grammars) {
grammar.deactivate();
}
for (let settings of this.settings) {
settings.deactivate(this.config);
}
if (this.stylesheetDisposables) this.stylesheetDisposables.dispose();
if (this.activationDisposables) this.activationDisposables.dispose();
if (this.keymapDisposables) this.keymapDisposables.dispose();
this.stylesheetsActivated = false;
this.grammarsActivated = false;
this.settingsActivated = false;
this.menusActivated = false;
}
reloadStylesheets() {
try {
this.loadStylesheets();
} catch (error) {
this.handleError(
`Failed to reload the ${this.name} package stylesheets`,
error
);
}
if (this.stylesheetDisposables) this.stylesheetDisposables.dispose();
this.stylesheetDisposables = new CompositeDisposable();
this.stylesheetsActivated = false;
this.activateStylesheets();
}
requireMainModule() {
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
if (this.packageManager.packagesCache[this.name].main) {
this.mainModule = require(this.packageManager.packagesCache[this.name]
.main);
return this.mainModule;
}
} else if (this.mainModuleRequired) {
return this.mainModule;
} else if (!this.isCompatible()) {
const nativeModuleNames = this.incompatibleModules
.map(m => m.name)
.join(', ');
console.warn(dedent`
Failed to require the main module of '${
this.name
}' because it requires one or more incompatible native modules (${nativeModuleNames}).
Run \`apm rebuild\` in the package directory and restart Atom to resolve.\
`);
} else {
const mainModulePath = this.getMainModulePath();
if (fs.isFileSync(mainModulePath)) {
this.mainModuleRequired = true;
const previousViewProviderCount = this.viewRegistry.getViewProviderCount();
const previousDeserializerCount = this.deserializerManager.getDeserializerCount();
this.mainModule = require(mainModulePath);
if (
this.viewRegistry.getViewProviderCount() ===
previousViewProviderCount &&
this.deserializerManager.getDeserializerCount() ===
previousDeserializerCount
) {
localStorage.setItem(
this.getCanDeferMainModuleRequireStorageKey(),
'true'
);
}
return this.mainModule;
}
}
}
getMainModulePath() {
if (this.resolvedMainModulePath) return this.mainModulePath;
this.resolvedMainModulePath = true;
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
if (this.packageManager.packagesCache[this.name].main) {
this.mainModulePath = path.resolve(
this.packageManager.resourcePath,
'static',
this.packageManager.packagesCache[this.name].main
);
} else {
this.mainModulePath = null;
}
} else {
const mainModulePath = this.metadata.main
? path.join(this.path, this.metadata.main)
: path.join(this.path, 'index');
this.mainModulePath = fs.resolveExtension(mainModulePath, [
'',
...CompileCache.supportedExtensions
]);
}
return this.mainModulePath;
}
activationShouldBeDeferred() {
return (
!this.deserialized &&
(this.hasActivationCommands() ||
this.hasActivationHooks() ||
this.hasWorkspaceOpeners() ||
this.hasDeferredURIHandler())
);
}
hasActivationHooks() {
const hooks = this.getActivationHooks();
return hooks && hooks.length > 0;
}
hasWorkspaceOpeners() {
const openers = this.getWorkspaceOpeners();
return openers && openers.length > 0;
}
hasActivationCommands() {
const object = this.getActivationCommands();
for (let selector in object) {
const commands = object[selector];
if (commands.length > 0) return true;
}
return false;
}
hasDeferredURIHandler() {
const handler = this.getURIHandler();
return handler && handler.deferActivation !== false;
}
subscribeToDeferredActivation() {
this.subscribeToActivationCommands();
this.subscribeToActivationHooks();
this.subscribeToWorkspaceOpeners();
}
subscribeToActivationCommands() {
this.activationCommandSubscriptions = new CompositeDisposable();
const object = this.getActivationCommands();
for (let selector in object) {
const commands = object[selector];
for (let command of commands) {
((selector, command) => {
// Add dummy command so it appears in menu.
// The real command will be registered on package activation
try {
this.activationCommandSubscriptions.add(
this.commandRegistry.add(selector, command, function() {})
);
} catch (error) {
if (error.code === 'EBADSELECTOR') {
const metadataPath = path.join(this.path, 'package.json');
error.message += ` in ${metadataPath}`;
error.stack += `\n at ${metadataPath}:1:1`;
}
throw error;
}
this.activationCommandSubscriptions.add(
this.commandRegistry.onWillDispatch(event => {
if (event.type !== command) return;
let currentTarget = event.target;
while (currentTarget) {
if (currentTarget.webkitMatchesSelector(selector)) {
this.activationCommandSubscriptions.dispose();
this.activateNow();
break;
}
currentTarget = currentTarget.parentElement;
}
})
);
})(selector, command);
}
}
}
getActivationCommands() {
if (this.activationCommands) return this.activationCommands;
this.activationCommands = {};
if (this.metadata.activationCommands) {
for (let selector in this.metadata.activationCommands) {
const commands = this.metadata.activationCommands[selector];
if (!this.activationCommands[selector])
this.activationCommands[selector] = [];
if (typeof commands === 'string') {
this.activationCommands[selector].push(commands);
} else if (Array.isArray(commands)) {
this.activationCommands[selector].push(...commands);
}
}
}
return this.activationCommands;
}
subscribeToActivationHooks() {
this.activationHookSubscriptions = new CompositeDisposable();
for (let hook of this.getActivationHooks()) {
if (typeof hook === 'string' && hook.trim().length > 0) {
this.activationHookSubscriptions.add(
this.packageManager.onDidTriggerActivationHook(hook, () =>
this.activateNow()
)
);
}
}
}
getActivationHooks() {
if (this.metadata && this.activationHooks) return this.activationHooks;
if (this.metadata.activationHooks) {
if (Array.isArray(this.metadata.activationHooks)) {
this.activationHooks = Array.from(
new Set(this.metadata.activationHooks)
);
} else if (typeof this.metadata.activationHooks === 'string') {
this.activationHooks = [this.metadata.activationHooks];
} else {
this.activationHooks = [];
}
} else {
this.activationHooks = [];
}
return this.activationHooks;
}
subscribeToWorkspaceOpeners() {
this.workspaceOpenerSubscriptions = new CompositeDisposable();
for (let opener of this.getWorkspaceOpeners()) {
this.workspaceOpenerSubscriptions.add(
atom.workspace.addOpener(filePath => {
if (filePath === opener) {
this.activateNow();
this.workspaceOpenerSubscriptions.dispose();
return atom.workspace.createItemForURI(opener);
}
})
);
}
}
getWorkspaceOpeners() {
if (this.workspaceOpeners) return this.workspaceOpeners;
if (this.metadata.workspaceOpeners) {
if (Array.isArray(this.metadata.workspaceOpeners)) {
this.workspaceOpeners = Array.from(
new Set(this.metadata.workspaceOpeners)
);
} else if (typeof this.metadata.workspaceOpeners === 'string') {
this.workspaceOpeners = [this.metadata.workspaceOpeners];
} else {
this.workspaceOpeners = [];
}
} else {
this.workspaceOpeners = [];
}
return this.workspaceOpeners;
}
getURIHandler() {
return this.metadata && this.metadata.uriHandler;
}
// Does the given module path contain native code?
isNativeModule(modulePath) {
try {
return (
fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node'])
.length > 0
);
} catch (error) {
return false;
}
}
// Get an array of all the native modules that this package depends on.
//
// First try to get this information from
// @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't
// exist, recurse through all dependencies.
getNativeModuleDependencyPaths() {
const nativeModulePaths = [];
if (this.metadata._atomModuleCache) {
const relativeNativeModuleBindingPaths =
(this.metadata._atomModuleCache.extensions &&
this.metadata._atomModuleCache.extensions['.node']) ||
[];
for (let relativeNativeModuleBindingPath of relativeNativeModuleBindingPaths) {
const nativeModulePath = path.join(
this.path,
relativeNativeModuleBindingPath,
'..',
'..',
'..'
);
nativeModulePaths.push(nativeModulePath);
}
return nativeModulePaths;
}
var traversePath = nodeModulesPath => {
try {
for (let modulePath of fs.listSync(nodeModulesPath)) {
if (this.isNativeModule(modulePath))
nativeModulePaths.push(modulePath);
traversePath(path.join(modulePath, 'node_modules'));
}
} catch (error) {}
};
traversePath(path.join(this.path, 'node_modules'));
return nativeModulePaths;
}
/*
Section: Native Module Compatibility
*/
// Extended: Are all native modules depended on by this package correctly
// compiled against the current version of Atom?
//
// Incompatible packages cannot be activated.
//
// Returns a {Boolean}, true if compatible, false if incompatible.
isCompatible() {
if (this.compatible == null) {
if (this.preloadedPackage) {
this.compatible = true;
} else if (this.getMainModulePath()) {
this.incompatibleModules = this.getIncompatibleNativeModules();
this.compatible =
this.incompatibleModules.length === 0 &&
this.getBuildFailureOutput() == null;
} else {
this.compatible = true;
}
}
return this.compatible;
}
// Extended: Rebuild native modules in this package's dependencies for the
// current version of Atom.
//
// Returns a {Promise} that resolves with an object containing `code`,
// `stdout`, and `stderr` properties based on the results of running
// `apm rebuild` on the package.
rebuild() {
return new Promise(resolve =>
this.runRebuildProcess(result => {
if (result.code === 0) {
global.localStorage.removeItem(
this.getBuildFailureOutputStorageKey()
);
} else {
this.compatible = false;
global.localStorage.setItem(
this.getBuildFailureOutputStorageKey(),
result.stderr
);
}
global.localStorage.setItem(
this.getIncompatibleNativeModulesStorageKey(),
'[]'
);
resolve(result);
})
);
}
// Extended: If a previous rebuild failed, get the contents of stderr.
//
// Returns a {String} or null if no previous build failure occurred.
getBuildFailureOutput() {
return global.localStorage.getItem(this.getBuildFailureOutputStorageKey());
}
runRebuildProcess(done) {
let stderr = '';
let stdout = '';
return new BufferedProcess({
command: this.packageManager.getApmPath(),
args: ['rebuild', '--no-color'],
options: { cwd: this.path },
stderr(output) {
stderr += output;
},
stdout(output) {
stdout += output;
},
exit(code) {
done({ code, stdout, stderr });
}
});
}
getBuildFailureOutputStorageKey() {
return `installed-packages:${this.name}:${
this.metadata.version
}:build-error`;
}
getIncompatibleNativeModulesStorageKey() {
const electronVersion = process.versions.electron;
return `installed-packages:${this.name}:${
this.metadata.version
}:electron-${electronVersion}:incompatible-native-modules`;
}
getCanDeferMainModuleRequireStorageKey() {
return `installed-packages:${this.name}:${
this.metadata.version
}:can-defer-main-module-require`;
}
// Get the incompatible native modules that this package depends on.
// This recurses through all dependencies and requires all modules that
// contain a `.node` file.
//
// This information is cached in local storage on a per package/version basis
// to minimize the impact on startup time.
getIncompatibleNativeModules() {
if (!this.packageManager.devMode) {
try {
const arrayAsString = global.localStorage.getItem(
this.getIncompatibleNativeModulesStorageKey()
);
if (arrayAsString) return JSON.parse(arrayAsString);
} catch (error1) {}
}
const incompatibleNativeModules = [];
for (let nativeModulePath of this.getNativeModuleDependencyPaths()) {
try {
require(nativeModulePath);
} catch (error) {
let version;
try {
({ version } = require(`${nativeModulePath}/package.json`));
} catch (error2) {}
incompatibleNativeModules.push({
path: nativeModulePath,
name: path.basename(nativeModulePath),
version,
error: error.message
});
}
}
global.localStorage.setItem(
this.getIncompatibleNativeModulesStorageKey(),
JSON.stringify(incompatibleNativeModules)
);
return incompatibleNativeModules;
}
handleError(message, error) {
if (atom.inSpecMode()) throw error;
let detail, location, stack;
if (error.filename && error.location && error instanceof SyntaxError) {
location = `${error.filename}:${error.location.first_line + 1}:${error
.location.first_column + 1}`;
detail = `${error.message} in ${location}`;
stack = 'SyntaxError: ' + error.message + '\n' + 'at ' + location;
} else if (
error.less &&
error.filename &&
error.column != null &&
error.line != null
) {
location = `${error.filename}:${error.line}:${error.column}`;
detail = `${error.message} in ${location}`;
stack = 'LessError: ' + error.message + '\n' + 'at ' + location;
} else {
detail = error.message;
stack = error.stack || error;
}
this.notificationManager.addFatalError(message, {
stack,
detail,
packageName: this.name,
dismissable: true
});
}
};
class SettingsFile {
static load(path, callback) {
CSON.readFile(path, (error, properties = {}) => {
if (error) {
callback(error);
} else {
callback(null, new SettingsFile(path, properties));
}
});
}
constructor(path, properties) {
this.path = path;
this.properties = properties;
}
activate(config) {
for (let selector in this.properties) {
config.set(null, this.properties[selector], {
scopeSelector: selector,
source: this.path
});
}
}
deactivate(config) {
for (let selector in this.properties) {
config.unset(null, { scopeSelector: selector, source: this.path });
}
}
}