/ src / package.js
package.js
   1  const path = require('path');
   2  const async = require('async');
   3  const CSON = require('season');
   4  const fs = require('fs-plus');
   5  const { Emitter, CompositeDisposable } = require('event-kit');
   6  const dedent = require('dedent');
   7  
   8  const CompileCache = require('./compile-cache');
   9  const ModuleCache = require('./module-cache');
  10  const BufferedProcess = require('./buffered-process');
  11  
  12  // Extended: Loads and activates a package's main module and resources such as
  13  // stylesheets, keymaps, grammar, editor properties, and menus.
  14  module.exports = class Package {
  15    /*
  16    Section: Construction
  17    */
  18  
  19    constructor(params) {
  20      this.config = params.config;
  21      this.packageManager = params.packageManager;
  22      this.styleManager = params.styleManager;
  23      this.commandRegistry = params.commandRegistry;
  24      this.keymapManager = params.keymapManager;
  25      this.notificationManager = params.notificationManager;
  26      this.grammarRegistry = params.grammarRegistry;
  27      this.themeManager = params.themeManager;
  28      this.menuManager = params.menuManager;
  29      this.contextMenuManager = params.contextMenuManager;
  30      this.deserializerManager = params.deserializerManager;
  31      this.viewRegistry = params.viewRegistry;
  32      this.emitter = new Emitter();
  33  
  34      this.mainModule = null;
  35      this.path = params.path;
  36      this.preloadedPackage = params.preloadedPackage;
  37      this.metadata =
  38        params.metadata || this.packageManager.loadPackageMetadata(this.path);
  39      this.bundledPackage =
  40        params.bundledPackage != null
  41          ? params.bundledPackage
  42          : this.packageManager.isBundledPackagePath(this.path);
  43      this.name =
  44        (this.metadata && this.metadata.name) ||
  45        params.name ||
  46        path.basename(this.path);
  47      this.reset();
  48    }
  49  
  50    /*
  51    Section: Event Subscription
  52    */
  53  
  54    // Essential: Invoke the given callback when all packages have been activated.
  55    //
  56    // * `callback` {Function}
  57    //
  58    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  59    onDidDeactivate(callback) {
  60      return this.emitter.on('did-deactivate', callback);
  61    }
  62  
  63    /*
  64    Section: Instance Methods
  65    */
  66  
  67    enable() {
  68      return this.config.removeAtKeyPath('core.disabledPackages', this.name);
  69    }
  70  
  71    disable() {
  72      return this.config.pushAtKeyPath('core.disabledPackages', this.name);
  73    }
  74  
  75    isTheme() {
  76      return this.metadata && this.metadata.theme;
  77    }
  78  
  79    measure(key, fn) {
  80      const startTime = Date.now();
  81      const value = fn();
  82      this[key] = Date.now() - startTime;
  83      return value;
  84    }
  85  
  86    getType() {
  87      return 'atom';
  88    }
  89  
  90    getStyleSheetPriority() {
  91      return 0;
  92    }
  93  
  94    preload() {
  95      this.loadKeymaps();
  96      this.loadMenus();
  97      this.registerDeserializerMethods();
  98      this.activateCoreStartupServices();
  99      this.registerURIHandler();
 100      this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata();
 101      this.requireMainModule();
 102      this.settingsPromise = this.loadSettings();
 103  
 104      this.activationDisposables = new CompositeDisposable();
 105      this.activateKeymaps();
 106      this.activateMenus();
 107      for (let settings of this.settings) {
 108        settings.activate(this.config);
 109      }
 110      this.settingsActivated = true;
 111    }
 112  
 113    finishLoading() {
 114      this.measure('loadTime', () => {
 115        this.path = path.join(this.packageManager.resourcePath, this.path);
 116        ModuleCache.add(this.path, this.metadata);
 117  
 118        this.loadStylesheets();
 119        // Unfortunately some packages are accessing `@mainModulePath`, so we need
 120        // to compute that variable eagerly also for preloaded packages.
 121        this.getMainModulePath();
 122      });
 123    }
 124  
 125    load() {
 126      this.measure('loadTime', () => {
 127        try {
 128          ModuleCache.add(this.path, this.metadata);
 129  
 130          this.loadKeymaps();
 131          this.loadMenus();
 132          this.loadStylesheets();
 133          this.registerDeserializerMethods();
 134          this.activateCoreStartupServices();
 135          this.registerURIHandler();
 136          this.registerTranspilerConfig();
 137          this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata();
 138          this.settingsPromise = this.loadSettings();
 139          if (this.shouldRequireMainModuleOnLoad() && this.mainModule == null) {
 140            this.requireMainModule();
 141          }
 142        } catch (error) {
 143          this.handleError(`Failed to load the ${this.name} package`, error);
 144        }
 145      });
 146      return this;
 147    }
 148  
 149    unload() {
 150      this.unregisterTranspilerConfig();
 151    }
 152  
 153    shouldRequireMainModuleOnLoad() {
 154      return !(
 155        this.metadata.deserializers ||
 156        this.metadata.viewProviders ||
 157        this.metadata.configSchema ||
 158        this.activationShouldBeDeferred() ||
 159        localStorage.getItem(this.getCanDeferMainModuleRequireStorageKey()) ===
 160          'true'
 161      );
 162    }
 163  
 164    reset() {
 165      this.stylesheets = [];
 166      this.keymaps = [];
 167      this.menus = [];
 168      this.grammars = [];
 169      this.settings = [];
 170      this.mainInitialized = false;
 171      this.mainActivated = false;
 172      this.deserialized = false;
 173    }
 174  
 175    initializeIfNeeded() {
 176      if (this.mainInitialized) return;
 177      this.measure('initializeTime', () => {
 178        try {
 179          // The main module's `initialize()` method is guaranteed to be called
 180          // before its `activate()`. This gives you a chance to handle the
 181          // serialized package state before the package's derserializers and view
 182          // providers are used.
 183          if (!this.mainModule) this.requireMainModule();
 184          if (typeof this.mainModule.initialize === 'function') {
 185            this.mainModule.initialize(
 186              this.packageManager.getPackageState(this.name) || {}
 187            );
 188          }
 189          this.mainInitialized = true;
 190        } catch (error) {
 191          this.handleError(
 192            `Failed to initialize the ${this.name} package`,
 193            error
 194          );
 195        }
 196      });
 197    }
 198  
 199    activate() {
 200      if (!this.grammarsPromise) this.grammarsPromise = this.loadGrammars();
 201      if (!this.activationPromise) {
 202        this.activationPromise = new Promise((resolve, reject) => {
 203          this.resolveActivationPromise = resolve;
 204          this.measure('activateTime', () => {
 205            try {
 206              this.activateResources();
 207              if (this.activationShouldBeDeferred()) {
 208                return this.subscribeToDeferredActivation();
 209              } else {
 210                return this.activateNow();
 211              }
 212            } catch (error) {
 213              return this.handleError(
 214                `Failed to activate the ${this.name} package`,
 215                error
 216              );
 217            }
 218          });
 219        });
 220      }
 221  
 222      return Promise.all([
 223        this.grammarsPromise,
 224        this.settingsPromise,
 225        this.activationPromise
 226      ]);
 227    }
 228  
 229    activateNow() {
 230      try {
 231        if (!this.mainModule) this.requireMainModule();
 232        this.configSchemaRegisteredOnActivate = this.registerConfigSchemaFromMainModule();
 233        this.registerViewProviders();
 234        this.activateStylesheets();
 235        if (this.mainModule && !this.mainActivated) {
 236          this.initializeIfNeeded();
 237          if (typeof this.mainModule.activateConfig === 'function') {
 238            this.mainModule.activateConfig();
 239          }
 240          if (typeof this.mainModule.activate === 'function') {
 241            this.mainModule.activate(
 242              this.packageManager.getPackageState(this.name) || {}
 243            );
 244          }
 245          this.mainActivated = true;
 246          this.activateServices();
 247        }
 248        if (this.activationCommandSubscriptions)
 249          this.activationCommandSubscriptions.dispose();
 250        if (this.activationHookSubscriptions)
 251          this.activationHookSubscriptions.dispose();
 252        if (this.workspaceOpenerSubscriptions)
 253          this.workspaceOpenerSubscriptions.dispose();
 254      } catch (error) {
 255        this.handleError(`Failed to activate the ${this.name} package`, error);
 256      }
 257  
 258      if (typeof this.resolveActivationPromise === 'function')
 259        this.resolveActivationPromise();
 260    }
 261  
 262    registerConfigSchemaFromMetadata() {
 263      const configSchema = this.metadata.configSchema;
 264      if (configSchema) {
 265        this.config.setSchema(this.name, {
 266          type: 'object',
 267          properties: configSchema
 268        });
 269        return true;
 270      } else {
 271        return false;
 272      }
 273    }
 274  
 275    registerConfigSchemaFromMainModule() {
 276      if (this.mainModule && !this.configSchemaRegisteredOnLoad) {
 277        if (typeof this.mainModule.config === 'object') {
 278          this.config.setSchema(this.name, {
 279            type: 'object',
 280            properties: this.mainModule.config
 281          });
 282          return true;
 283        }
 284      }
 285      return false;
 286    }
 287  
 288    // TODO: Remove. Settings view calls this method currently.
 289    activateConfig() {
 290      if (this.configSchemaRegisteredOnLoad) return;
 291      this.requireMainModule();
 292      this.registerConfigSchemaFromMainModule();
 293    }
 294  
 295    activateStylesheets() {
 296      if (this.stylesheetsActivated) return;
 297  
 298      this.stylesheetDisposables = new CompositeDisposable();
 299  
 300      const priority = this.getStyleSheetPriority();
 301      for (let [sourcePath, source] of this.stylesheets) {
 302        const match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./);
 303  
 304        let context;
 305        if (match) {
 306          context = match[1];
 307        } else if (this.metadata.theme === 'syntax') {
 308          context = 'atom-text-editor';
 309        }
 310  
 311        this.stylesheetDisposables.add(
 312          this.styleManager.addStyleSheet(source, {
 313            sourcePath,
 314            priority,
 315            context,
 316            skipDeprecatedSelectorsTransformation: this.bundledPackage
 317          })
 318        );
 319      }
 320  
 321      this.stylesheetsActivated = true;
 322    }
 323  
 324    activateResources() {
 325      if (!this.activationDisposables)
 326        this.activationDisposables = new CompositeDisposable();
 327  
 328      const packagesWithKeymapsDisabled = this.config.get(
 329        'core.packagesWithKeymapsDisabled'
 330      );
 331      if (
 332        packagesWithKeymapsDisabled &&
 333        packagesWithKeymapsDisabled.includes(this.name)
 334      ) {
 335        this.deactivateKeymaps();
 336      } else if (!this.keymapActivated) {
 337        this.activateKeymaps();
 338      }
 339  
 340      if (!this.menusActivated) {
 341        this.activateMenus();
 342      }
 343  
 344      if (!this.grammarsActivated) {
 345        for (let grammar of this.grammars) {
 346          grammar.activate();
 347        }
 348        this.grammarsActivated = true;
 349      }
 350  
 351      if (!this.settingsActivated) {
 352        for (let settings of this.settings) {
 353          settings.activate(this.config);
 354        }
 355        this.settingsActivated = true;
 356      }
 357    }
 358  
 359    activateKeymaps() {
 360      if (this.keymapActivated) return;
 361  
 362      this.keymapDisposables = new CompositeDisposable();
 363  
 364      const validateSelectors = !this.preloadedPackage;
 365      for (let [keymapPath, map] of this.keymaps) {
 366        this.keymapDisposables.add(
 367          this.keymapManager.add(keymapPath, map, 0, validateSelectors)
 368        );
 369      }
 370      this.menuManager.update();
 371  
 372      this.keymapActivated = true;
 373    }
 374  
 375    deactivateKeymaps() {
 376      if (!this.keymapActivated) return;
 377      if (this.keymapDisposables) {
 378        this.keymapDisposables.dispose();
 379      }
 380      this.menuManager.update();
 381      this.keymapActivated = false;
 382    }
 383  
 384    hasKeymaps() {
 385      for (let [, map] of this.keymaps) {
 386        if (map.length > 0) return true;
 387      }
 388      return false;
 389    }
 390  
 391    activateMenus() {
 392      const validateSelectors = !this.preloadedPackage;
 393      for (const [menuPath, map] of this.menus) {
 394        if (map['context-menu']) {
 395          try {
 396            const itemsBySelector = map['context-menu'];
 397            this.activationDisposables.add(
 398              this.contextMenuManager.add(itemsBySelector, validateSelectors)
 399            );
 400          } catch (error) {
 401            if (error.code === 'EBADSELECTOR') {
 402              error.message += ` in ${menuPath}`;
 403              error.stack += `\n  at ${menuPath}:1:1`;
 404            }
 405            throw error;
 406          }
 407        }
 408      }
 409  
 410      for (const [, map] of this.menus) {
 411        if (map.menu)
 412          this.activationDisposables.add(this.menuManager.add(map.menu));
 413      }
 414  
 415      this.menusActivated = true;
 416    }
 417  
 418    activateServices() {
 419      let methodName, version, versions;
 420      for (var name in this.metadata.providedServices) {
 421        ({ versions } = this.metadata.providedServices[name]);
 422        const servicesByVersion = {};
 423        for (version in versions) {
 424          methodName = versions[version];
 425          if (typeof this.mainModule[methodName] === 'function') {
 426            servicesByVersion[version] = this.mainModule[methodName]();
 427          }
 428        }
 429        this.activationDisposables.add(
 430          this.packageManager.serviceHub.provide(name, servicesByVersion)
 431        );
 432      }
 433  
 434      for (name in this.metadata.consumedServices) {
 435        ({ versions } = this.metadata.consumedServices[name]);
 436        for (version in versions) {
 437          methodName = versions[version];
 438          if (typeof this.mainModule[methodName] === 'function') {
 439            this.activationDisposables.add(
 440              this.packageManager.serviceHub.consume(
 441                name,
 442                version,
 443                this.mainModule[methodName].bind(this.mainModule)
 444              )
 445            );
 446          }
 447        }
 448      }
 449    }
 450  
 451    registerURIHandler() {
 452      const handlerConfig = this.getURIHandler();
 453      const methodName = handlerConfig && handlerConfig.method;
 454      if (methodName) {
 455        this.uriHandlerSubscription = this.packageManager.registerURIHandlerForPackage(
 456          this.name,
 457          (...args) => this.handleURI(methodName, args)
 458        );
 459      }
 460    }
 461  
 462    unregisterURIHandler() {
 463      if (this.uriHandlerSubscription) this.uriHandlerSubscription.dispose();
 464    }
 465  
 466    handleURI(methodName, args) {
 467      this.activate().then(() => {
 468        if (this.mainModule[methodName])
 469          this.mainModule[methodName].apply(this.mainModule, args);
 470      });
 471      if (!this.mainActivated) this.activateNow();
 472    }
 473  
 474    registerTranspilerConfig() {
 475      if (this.metadata.atomTranspilers) {
 476        CompileCache.addTranspilerConfigForPath(
 477          this.path,
 478          this.name,
 479          this.metadata,
 480          this.metadata.atomTranspilers
 481        );
 482      }
 483    }
 484  
 485    unregisterTranspilerConfig() {
 486      if (this.metadata.atomTranspilers) {
 487        CompileCache.removeTranspilerConfigForPath(this.path);
 488      }
 489    }
 490  
 491    loadKeymaps() {
 492      if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
 493        this.keymaps = [];
 494        for (const keymapPath in this.packageManager.packagesCache[this.name]
 495          .keymaps) {
 496          const keymapObject = this.packageManager.packagesCache[this.name]
 497            .keymaps[keymapPath];
 498          this.keymaps.push([`core:${keymapPath}`, keymapObject]);
 499        }
 500      } else {
 501        this.keymaps = this.getKeymapPaths().map(keymapPath => [
 502          keymapPath,
 503          CSON.readFileSync(keymapPath, { allowDuplicateKeys: false }) || {}
 504        ]);
 505      }
 506    }
 507  
 508    loadMenus() {
 509      if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
 510        this.menus = [];
 511        for (const menuPath in this.packageManager.packagesCache[this.name]
 512          .menus) {
 513          const menuObject = this.packageManager.packagesCache[this.name].menus[
 514            menuPath
 515          ];
 516          this.menus.push([`core:${menuPath}`, menuObject]);
 517        }
 518      } else {
 519        this.menus = this.getMenuPaths().map(menuPath => [
 520          menuPath,
 521          CSON.readFileSync(menuPath) || {}
 522        ]);
 523      }
 524    }
 525  
 526    getKeymapPaths() {
 527      const keymapsDirPath = path.join(this.path, 'keymaps');
 528      if (this.metadata.keymaps) {
 529        return this.metadata.keymaps.map(name =>
 530          fs.resolve(keymapsDirPath, name, ['json', 'cson', ''])
 531        );
 532      } else {
 533        return fs.listSync(keymapsDirPath, ['cson', 'json']);
 534      }
 535    }
 536  
 537    getMenuPaths() {
 538      const menusDirPath = path.join(this.path, 'menus');
 539      if (this.metadata.menus) {
 540        return this.metadata.menus.map(name =>
 541          fs.resolve(menusDirPath, name, ['json', 'cson', ''])
 542        );
 543      } else {
 544        return fs.listSync(menusDirPath, ['cson', 'json']);
 545      }
 546    }
 547  
 548    loadStylesheets() {
 549      this.stylesheets = this.getStylesheetPaths().map(stylesheetPath => [
 550        stylesheetPath,
 551        this.themeManager.loadStylesheet(stylesheetPath, true)
 552      ]);
 553    }
 554  
 555    registerDeserializerMethods() {
 556      if (this.metadata.deserializers) {
 557        Object.keys(this.metadata.deserializers).forEach(deserializerName => {
 558          const methodName = this.metadata.deserializers[deserializerName];
 559          this.deserializerManager.add({
 560            name: deserializerName,
 561            deserialize: (state, atomEnvironment) => {
 562              this.registerViewProviders();
 563              this.requireMainModule();
 564              this.initializeIfNeeded();
 565              if (atomEnvironment.packages.hasActivatedInitialPackages()) {
 566                // Only explicitly activate the package if initial packages
 567                // have finished activating. This is because deserialization
 568                // generally occurs at Atom startup, which happens before the
 569                // workspace element is added to the DOM and is inconsistent with
 570                // with when initial package activation occurs. Triggering activation
 571                // immediately may cause problems with packages that expect to
 572                // always have access to the workspace element.
 573                // Otherwise, we just set the deserialized flag and package-manager
 574                // will activate this package as normal during initial package activation.
 575                this.activateNow();
 576              }
 577              this.deserialized = true;
 578              return this.mainModule[methodName](state, atomEnvironment);
 579            }
 580          });
 581        });
 582      }
 583    }
 584  
 585    activateCoreStartupServices() {
 586      const directoryProviderService =
 587        this.metadata.providedServices &&
 588        this.metadata.providedServices['atom.directory-provider'];
 589      if (directoryProviderService) {
 590        this.requireMainModule();
 591        const servicesByVersion = {};
 592        for (let version in directoryProviderService.versions) {
 593          const methodName = directoryProviderService.versions[version];
 594          if (typeof this.mainModule[methodName] === 'function') {
 595            servicesByVersion[version] = this.mainModule[methodName]();
 596          }
 597        }
 598        this.packageManager.serviceHub.provide(
 599          'atom.directory-provider',
 600          servicesByVersion
 601        );
 602      }
 603    }
 604  
 605    registerViewProviders() {
 606      if (this.metadata.viewProviders && !this.registeredViewProviders) {
 607        this.requireMainModule();
 608        this.metadata.viewProviders.forEach(methodName => {
 609          this.viewRegistry.addViewProvider(model => {
 610            this.initializeIfNeeded();
 611            return this.mainModule[methodName](model);
 612          });
 613        });
 614        this.registeredViewProviders = true;
 615      }
 616    }
 617  
 618    getStylesheetsPath() {
 619      return path.join(this.path, 'styles');
 620    }
 621  
 622    getStylesheetPaths() {
 623      if (
 624        this.bundledPackage &&
 625        this.packageManager.packagesCache[this.name] &&
 626        this.packageManager.packagesCache[this.name].styleSheetPaths
 627      ) {
 628        const { styleSheetPaths } = this.packageManager.packagesCache[this.name];
 629        return styleSheetPaths.map(styleSheetPath =>
 630          path.join(this.path, styleSheetPath)
 631        );
 632      } else {
 633        let indexStylesheet;
 634        const stylesheetDirPath = this.getStylesheetsPath();
 635        if (this.metadata.mainStyleSheet) {
 636          return [fs.resolve(this.path, this.metadata.mainStyleSheet)];
 637        } else if (this.metadata.styleSheets) {
 638          return this.metadata.styleSheets.map(name =>
 639            fs.resolve(stylesheetDirPath, name, ['css', 'less', ''])
 640          );
 641        } else if (
 642          (indexStylesheet = fs.resolve(this.path, 'index', ['css', 'less']))
 643        ) {
 644          return [indexStylesheet];
 645        } else {
 646          return fs.listSync(stylesheetDirPath, ['css', 'less']);
 647        }
 648      }
 649    }
 650  
 651    loadGrammarsSync() {
 652      if (this.grammarsLoaded) return;
 653  
 654      let grammarPaths;
 655      if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
 656        ({ grammarPaths } = this.packageManager.packagesCache[this.name]);
 657      } else {
 658        grammarPaths = fs.listSync(path.join(this.path, 'grammars'), [
 659          'json',
 660          'cson'
 661        ]);
 662      }
 663  
 664      for (let grammarPath of grammarPaths) {
 665        if (
 666          this.preloadedPackage &&
 667          this.packageManager.packagesCache[this.name]
 668        ) {
 669          grammarPath = path.resolve(
 670            this.packageManager.resourcePath,
 671            grammarPath
 672          );
 673        }
 674  
 675        try {
 676          const grammar = this.grammarRegistry.readGrammarSync(grammarPath);
 677          grammar.packageName = this.name;
 678          grammar.bundledPackage = this.bundledPackage;
 679          this.grammars.push(grammar);
 680          grammar.activate();
 681        } catch (error) {
 682          console.warn(
 683            `Failed to load grammar: ${grammarPath}`,
 684            error.stack || error
 685          );
 686        }
 687      }
 688  
 689      this.grammarsLoaded = true;
 690      this.grammarsActivated = true;
 691    }
 692  
 693    loadGrammars() {
 694      if (this.grammarsLoaded) return Promise.resolve();
 695  
 696      const loadGrammar = (grammarPath, callback) => {
 697        if (this.preloadedPackage) {
 698          grammarPath = path.resolve(
 699            this.packageManager.resourcePath,
 700            grammarPath
 701          );
 702        }
 703  
 704        return this.grammarRegistry.readGrammar(grammarPath, (error, grammar) => {
 705          if (error) {
 706            const detail = `${error.message} in ${grammarPath}`;
 707            const stack = `${error.stack}\n  at ${grammarPath}:1:1`;
 708            this.notificationManager.addFatalError(
 709              `Failed to load a ${this.name} package grammar`,
 710              { stack, detail, packageName: this.name, dismissable: true }
 711            );
 712          } else {
 713            grammar.packageName = this.name;
 714            grammar.bundledPackage = this.bundledPackage;
 715            this.grammars.push(grammar);
 716            if (this.grammarsActivated) grammar.activate();
 717          }
 718          return callback();
 719        });
 720      };
 721  
 722      return new Promise(resolve => {
 723        if (
 724          this.preloadedPackage &&
 725          this.packageManager.packagesCache[this.name]
 726        ) {
 727          const { grammarPaths } = this.packageManager.packagesCache[this.name];
 728          return async.each(grammarPaths, loadGrammar, () => resolve());
 729        } else {
 730          const grammarsDirPath = path.join(this.path, 'grammars');
 731          fs.exists(grammarsDirPath, grammarsDirExists => {
 732            if (!grammarsDirExists) return resolve();
 733            fs.list(grammarsDirPath, ['json', 'cson'], (error, grammarPaths) => {
 734              if (error || !grammarPaths) return resolve();
 735              async.each(grammarPaths, loadGrammar, () => resolve());
 736            });
 737          });
 738        }
 739      });
 740    }
 741  
 742    loadSettings() {
 743      this.settings = [];
 744  
 745      const loadSettingsFile = (settingsPath, callback) => {
 746        return SettingsFile.load(settingsPath, (error, settingsFile) => {
 747          if (error) {
 748            const detail = `${error.message} in ${settingsPath}`;
 749            const stack = `${error.stack}\n  at ${settingsPath}:1:1`;
 750            this.notificationManager.addFatalError(
 751              `Failed to load the ${this.name} package settings`,
 752              { stack, detail, packageName: this.name, dismissable: true }
 753            );
 754          } else {
 755            this.settings.push(settingsFile);
 756            if (this.settingsActivated) settingsFile.activate(this.config);
 757          }
 758          return callback();
 759        });
 760      };
 761  
 762      if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
 763        for (let settingsPath in this.packageManager.packagesCache[this.name]
 764          .settings) {
 765          const properties = this.packageManager.packagesCache[this.name]
 766            .settings[settingsPath];
 767          const settingsFile = new SettingsFile(
 768            `core:${settingsPath}`,
 769            properties || {}
 770          );
 771          this.settings.push(settingsFile);
 772          if (this.settingsActivated) settingsFile.activate(this.config);
 773        }
 774      } else {
 775        return new Promise(resolve => {
 776          const settingsDirPath = path.join(this.path, 'settings');
 777          fs.exists(settingsDirPath, settingsDirExists => {
 778            if (!settingsDirExists) return resolve();
 779            fs.list(settingsDirPath, ['json', 'cson'], (error, settingsPaths) => {
 780              if (error || !settingsPaths) return resolve();
 781              async.each(settingsPaths, loadSettingsFile, () => resolve());
 782            });
 783          });
 784        });
 785      }
 786    }
 787  
 788    serialize() {
 789      if (this.mainActivated) {
 790        if (typeof this.mainModule.serialize === 'function') {
 791          try {
 792            return this.mainModule.serialize();
 793          } catch (error) {
 794            console.error(
 795              `Error serializing package '${this.name}'`,
 796              error.stack
 797            );
 798          }
 799        }
 800      }
 801    }
 802  
 803    async deactivate() {
 804      this.activationPromise = null;
 805      this.resolveActivationPromise = null;
 806      if (this.activationCommandSubscriptions)
 807        this.activationCommandSubscriptions.dispose();
 808      if (this.activationHookSubscriptions)
 809        this.activationHookSubscriptions.dispose();
 810      this.configSchemaRegisteredOnActivate = false;
 811      this.unregisterURIHandler();
 812      this.deactivateResources();
 813      this.deactivateKeymaps();
 814  
 815      if (!this.mainActivated) {
 816        this.emitter.emit('did-deactivate');
 817        return;
 818      }
 819  
 820      if (typeof this.mainModule.deactivate === 'function') {
 821        try {
 822          const deactivationResult = this.mainModule.deactivate();
 823          if (
 824            deactivationResult &&
 825            typeof deactivationResult.then === 'function'
 826          ) {
 827            await deactivationResult;
 828          }
 829        } catch (error) {
 830          console.error(`Error deactivating package '${this.name}'`, error.stack);
 831        }
 832      }
 833  
 834      if (typeof this.mainModule.deactivateConfig === 'function') {
 835        try {
 836          await this.mainModule.deactivateConfig();
 837        } catch (error) {
 838          console.error(`Error deactivating package '${this.name}'`, error.stack);
 839        }
 840      }
 841  
 842      this.mainActivated = false;
 843      this.mainInitialized = false;
 844      this.emitter.emit('did-deactivate');
 845    }
 846  
 847    deactivateResources() {
 848      for (let grammar of this.grammars) {
 849        grammar.deactivate();
 850      }
 851      for (let settings of this.settings) {
 852        settings.deactivate(this.config);
 853      }
 854  
 855      if (this.stylesheetDisposables) this.stylesheetDisposables.dispose();
 856      if (this.activationDisposables) this.activationDisposables.dispose();
 857      if (this.keymapDisposables) this.keymapDisposables.dispose();
 858  
 859      this.stylesheetsActivated = false;
 860      this.grammarsActivated = false;
 861      this.settingsActivated = false;
 862      this.menusActivated = false;
 863    }
 864  
 865    reloadStylesheets() {
 866      try {
 867        this.loadStylesheets();
 868      } catch (error) {
 869        this.handleError(
 870          `Failed to reload the ${this.name} package stylesheets`,
 871          error
 872        );
 873      }
 874  
 875      if (this.stylesheetDisposables) this.stylesheetDisposables.dispose();
 876      this.stylesheetDisposables = new CompositeDisposable();
 877      this.stylesheetsActivated = false;
 878      this.activateStylesheets();
 879    }
 880  
 881    requireMainModule() {
 882      if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
 883        if (this.packageManager.packagesCache[this.name].main) {
 884          this.mainModule = require(this.packageManager.packagesCache[this.name]
 885            .main);
 886          return this.mainModule;
 887        }
 888      } else if (this.mainModuleRequired) {
 889        return this.mainModule;
 890      } else if (!this.isCompatible()) {
 891        const nativeModuleNames = this.incompatibleModules
 892          .map(m => m.name)
 893          .join(', ');
 894        console.warn(dedent`
 895          Failed to require the main module of '${
 896            this.name
 897          }' because it requires one or more incompatible native modules (${nativeModuleNames}).
 898          Run \`apm rebuild\` in the package directory and restart Atom to resolve.\
 899        `);
 900      } else {
 901        const mainModulePath = this.getMainModulePath();
 902        if (fs.isFileSync(mainModulePath)) {
 903          this.mainModuleRequired = true;
 904  
 905          const previousViewProviderCount = this.viewRegistry.getViewProviderCount();
 906          const previousDeserializerCount = this.deserializerManager.getDeserializerCount();
 907          this.mainModule = require(mainModulePath);
 908          if (
 909            this.viewRegistry.getViewProviderCount() ===
 910              previousViewProviderCount &&
 911            this.deserializerManager.getDeserializerCount() ===
 912              previousDeserializerCount
 913          ) {
 914            localStorage.setItem(
 915              this.getCanDeferMainModuleRequireStorageKey(),
 916              'true'
 917            );
 918          }
 919          return this.mainModule;
 920        }
 921      }
 922    }
 923  
 924    getMainModulePath() {
 925      if (this.resolvedMainModulePath) return this.mainModulePath;
 926      this.resolvedMainModulePath = true;
 927  
 928      if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
 929        if (this.packageManager.packagesCache[this.name].main) {
 930          this.mainModulePath = path.resolve(
 931            this.packageManager.resourcePath,
 932            'static',
 933            this.packageManager.packagesCache[this.name].main
 934          );
 935        } else {
 936          this.mainModulePath = null;
 937        }
 938      } else {
 939        const mainModulePath = this.metadata.main
 940          ? path.join(this.path, this.metadata.main)
 941          : path.join(this.path, 'index');
 942        this.mainModulePath = fs.resolveExtension(mainModulePath, [
 943          '',
 944          ...CompileCache.supportedExtensions
 945        ]);
 946      }
 947      return this.mainModulePath;
 948    }
 949  
 950    activationShouldBeDeferred() {
 951      return (
 952        !this.deserialized &&
 953        (this.hasActivationCommands() ||
 954          this.hasActivationHooks() ||
 955          this.hasWorkspaceOpeners() ||
 956          this.hasDeferredURIHandler())
 957      );
 958    }
 959  
 960    hasActivationHooks() {
 961      const hooks = this.getActivationHooks();
 962      return hooks && hooks.length > 0;
 963    }
 964  
 965    hasWorkspaceOpeners() {
 966      const openers = this.getWorkspaceOpeners();
 967      return openers && openers.length > 0;
 968    }
 969  
 970    hasActivationCommands() {
 971      const object = this.getActivationCommands();
 972      for (let selector in object) {
 973        const commands = object[selector];
 974        if (commands.length > 0) return true;
 975      }
 976      return false;
 977    }
 978  
 979    hasDeferredURIHandler() {
 980      const handler = this.getURIHandler();
 981      return handler && handler.deferActivation !== false;
 982    }
 983  
 984    subscribeToDeferredActivation() {
 985      this.subscribeToActivationCommands();
 986      this.subscribeToActivationHooks();
 987      this.subscribeToWorkspaceOpeners();
 988    }
 989  
 990    subscribeToActivationCommands() {
 991      this.activationCommandSubscriptions = new CompositeDisposable();
 992      const object = this.getActivationCommands();
 993      for (let selector in object) {
 994        const commands = object[selector];
 995        for (let command of commands) {
 996          ((selector, command) => {
 997            // Add dummy command so it appears in menu.
 998            // The real command will be registered on package activation
 999            try {
1000              this.activationCommandSubscriptions.add(
1001                this.commandRegistry.add(selector, command, function() {})
1002              );
1003            } catch (error) {
1004              if (error.code === 'EBADSELECTOR') {
1005                const metadataPath = path.join(this.path, 'package.json');
1006                error.message += ` in ${metadataPath}`;
1007                error.stack += `\n  at ${metadataPath}:1:1`;
1008              }
1009              throw error;
1010            }
1011  
1012            this.activationCommandSubscriptions.add(
1013              this.commandRegistry.onWillDispatch(event => {
1014                if (event.type !== command) return;
1015                let currentTarget = event.target;
1016                while (currentTarget) {
1017                  if (currentTarget.webkitMatchesSelector(selector)) {
1018                    this.activationCommandSubscriptions.dispose();
1019                    this.activateNow();
1020                    break;
1021                  }
1022                  currentTarget = currentTarget.parentElement;
1023                }
1024              })
1025            );
1026          })(selector, command);
1027        }
1028      }
1029    }
1030  
1031    getActivationCommands() {
1032      if (this.activationCommands) return this.activationCommands;
1033  
1034      this.activationCommands = {};
1035  
1036      if (this.metadata.activationCommands) {
1037        for (let selector in this.metadata.activationCommands) {
1038          const commands = this.metadata.activationCommands[selector];
1039          if (!this.activationCommands[selector])
1040            this.activationCommands[selector] = [];
1041          if (typeof commands === 'string') {
1042            this.activationCommands[selector].push(commands);
1043          } else if (Array.isArray(commands)) {
1044            this.activationCommands[selector].push(...commands);
1045          }
1046        }
1047      }
1048  
1049      return this.activationCommands;
1050    }
1051  
1052    subscribeToActivationHooks() {
1053      this.activationHookSubscriptions = new CompositeDisposable();
1054      for (let hook of this.getActivationHooks()) {
1055        if (typeof hook === 'string' && hook.trim().length > 0) {
1056          this.activationHookSubscriptions.add(
1057            this.packageManager.onDidTriggerActivationHook(hook, () =>
1058              this.activateNow()
1059            )
1060          );
1061        }
1062      }
1063    }
1064  
1065    getActivationHooks() {
1066      if (this.metadata && this.activationHooks) return this.activationHooks;
1067  
1068      if (this.metadata.activationHooks) {
1069        if (Array.isArray(this.metadata.activationHooks)) {
1070          this.activationHooks = Array.from(
1071            new Set(this.metadata.activationHooks)
1072          );
1073        } else if (typeof this.metadata.activationHooks === 'string') {
1074          this.activationHooks = [this.metadata.activationHooks];
1075        } else {
1076          this.activationHooks = [];
1077        }
1078      } else {
1079        this.activationHooks = [];
1080      }
1081  
1082      return this.activationHooks;
1083    }
1084  
1085    subscribeToWorkspaceOpeners() {
1086      this.workspaceOpenerSubscriptions = new CompositeDisposable();
1087      for (let opener of this.getWorkspaceOpeners()) {
1088        this.workspaceOpenerSubscriptions.add(
1089          atom.workspace.addOpener(filePath => {
1090            if (filePath === opener) {
1091              this.activateNow();
1092              this.workspaceOpenerSubscriptions.dispose();
1093              return atom.workspace.createItemForURI(opener);
1094            }
1095          })
1096        );
1097      }
1098    }
1099  
1100    getWorkspaceOpeners() {
1101      if (this.workspaceOpeners) return this.workspaceOpeners;
1102  
1103      if (this.metadata.workspaceOpeners) {
1104        if (Array.isArray(this.metadata.workspaceOpeners)) {
1105          this.workspaceOpeners = Array.from(
1106            new Set(this.metadata.workspaceOpeners)
1107          );
1108        } else if (typeof this.metadata.workspaceOpeners === 'string') {
1109          this.workspaceOpeners = [this.metadata.workspaceOpeners];
1110        } else {
1111          this.workspaceOpeners = [];
1112        }
1113      } else {
1114        this.workspaceOpeners = [];
1115      }
1116  
1117      return this.workspaceOpeners;
1118    }
1119  
1120    getURIHandler() {
1121      return this.metadata && this.metadata.uriHandler;
1122    }
1123  
1124    // Does the given module path contain native code?
1125    isNativeModule(modulePath) {
1126      try {
1127        return (
1128          fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node'])
1129            .length > 0
1130        );
1131      } catch (error) {
1132        return false;
1133      }
1134    }
1135  
1136    // Get an array of all the native modules that this package depends on.
1137    //
1138    // First try to get this information from
1139    // @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't
1140    // exist, recurse through all dependencies.
1141    getNativeModuleDependencyPaths() {
1142      const nativeModulePaths = [];
1143  
1144      if (this.metadata._atomModuleCache) {
1145        const relativeNativeModuleBindingPaths =
1146          (this.metadata._atomModuleCache.extensions &&
1147            this.metadata._atomModuleCache.extensions['.node']) ||
1148          [];
1149        for (let relativeNativeModuleBindingPath of relativeNativeModuleBindingPaths) {
1150          const nativeModulePath = path.join(
1151            this.path,
1152            relativeNativeModuleBindingPath,
1153            '..',
1154            '..',
1155            '..'
1156          );
1157          nativeModulePaths.push(nativeModulePath);
1158        }
1159        return nativeModulePaths;
1160      }
1161  
1162      var traversePath = nodeModulesPath => {
1163        try {
1164          for (let modulePath of fs.listSync(nodeModulesPath)) {
1165            if (this.isNativeModule(modulePath))
1166              nativeModulePaths.push(modulePath);
1167            traversePath(path.join(modulePath, 'node_modules'));
1168          }
1169        } catch (error) {}
1170      };
1171  
1172      traversePath(path.join(this.path, 'node_modules'));
1173  
1174      return nativeModulePaths;
1175    }
1176  
1177    /*
1178    Section: Native Module Compatibility
1179    */
1180  
1181    // Extended: Are all native modules depended on by this package correctly
1182    // compiled against the current version of Atom?
1183    //
1184    // Incompatible packages cannot be activated.
1185    //
1186    // Returns a {Boolean}, true if compatible, false if incompatible.
1187    isCompatible() {
1188      if (this.compatible == null) {
1189        if (this.preloadedPackage) {
1190          this.compatible = true;
1191        } else if (this.getMainModulePath()) {
1192          this.incompatibleModules = this.getIncompatibleNativeModules();
1193          this.compatible =
1194            this.incompatibleModules.length === 0 &&
1195            this.getBuildFailureOutput() == null;
1196        } else {
1197          this.compatible = true;
1198        }
1199      }
1200      return this.compatible;
1201    }
1202  
1203    // Extended: Rebuild native modules in this package's dependencies for the
1204    // current version of Atom.
1205    //
1206    // Returns a {Promise} that resolves with an object containing `code`,
1207    // `stdout`, and `stderr` properties based on the results of running
1208    // `apm rebuild` on the package.
1209    rebuild() {
1210      return new Promise(resolve =>
1211        this.runRebuildProcess(result => {
1212          if (result.code === 0) {
1213            global.localStorage.removeItem(
1214              this.getBuildFailureOutputStorageKey()
1215            );
1216          } else {
1217            this.compatible = false;
1218            global.localStorage.setItem(
1219              this.getBuildFailureOutputStorageKey(),
1220              result.stderr
1221            );
1222          }
1223          global.localStorage.setItem(
1224            this.getIncompatibleNativeModulesStorageKey(),
1225            '[]'
1226          );
1227          resolve(result);
1228        })
1229      );
1230    }
1231  
1232    // Extended: If a previous rebuild failed, get the contents of stderr.
1233    //
1234    // Returns a {String} or null if no previous build failure occurred.
1235    getBuildFailureOutput() {
1236      return global.localStorage.getItem(this.getBuildFailureOutputStorageKey());
1237    }
1238  
1239    runRebuildProcess(done) {
1240      let stderr = '';
1241      let stdout = '';
1242      return new BufferedProcess({
1243        command: this.packageManager.getApmPath(),
1244        args: ['rebuild', '--no-color'],
1245        options: { cwd: this.path },
1246        stderr(output) {
1247          stderr += output;
1248        },
1249        stdout(output) {
1250          stdout += output;
1251        },
1252        exit(code) {
1253          done({ code, stdout, stderr });
1254        }
1255      });
1256    }
1257  
1258    getBuildFailureOutputStorageKey() {
1259      return `installed-packages:${this.name}:${
1260        this.metadata.version
1261      }:build-error`;
1262    }
1263  
1264    getIncompatibleNativeModulesStorageKey() {
1265      const electronVersion = process.versions.electron;
1266      return `installed-packages:${this.name}:${
1267        this.metadata.version
1268      }:electron-${electronVersion}:incompatible-native-modules`;
1269    }
1270  
1271    getCanDeferMainModuleRequireStorageKey() {
1272      return `installed-packages:${this.name}:${
1273        this.metadata.version
1274      }:can-defer-main-module-require`;
1275    }
1276  
1277    // Get the incompatible native modules that this package depends on.
1278    // This recurses through all dependencies and requires all modules that
1279    // contain a `.node` file.
1280    //
1281    // This information is cached in local storage on a per package/version basis
1282    // to minimize the impact on startup time.
1283    getIncompatibleNativeModules() {
1284      if (!this.packageManager.devMode) {
1285        try {
1286          const arrayAsString = global.localStorage.getItem(
1287            this.getIncompatibleNativeModulesStorageKey()
1288          );
1289          if (arrayAsString) return JSON.parse(arrayAsString);
1290        } catch (error1) {}
1291      }
1292  
1293      const incompatibleNativeModules = [];
1294      for (let nativeModulePath of this.getNativeModuleDependencyPaths()) {
1295        try {
1296          require(nativeModulePath);
1297        } catch (error) {
1298          let version;
1299          try {
1300            ({ version } = require(`${nativeModulePath}/package.json`));
1301          } catch (error2) {}
1302          incompatibleNativeModules.push({
1303            path: nativeModulePath,
1304            name: path.basename(nativeModulePath),
1305            version,
1306            error: error.message
1307          });
1308        }
1309      }
1310  
1311      global.localStorage.setItem(
1312        this.getIncompatibleNativeModulesStorageKey(),
1313        JSON.stringify(incompatibleNativeModules)
1314      );
1315  
1316      return incompatibleNativeModules;
1317    }
1318  
1319    handleError(message, error) {
1320      if (atom.inSpecMode()) throw error;
1321  
1322      let detail, location, stack;
1323      if (error.filename && error.location && error instanceof SyntaxError) {
1324        location = `${error.filename}:${error.location.first_line + 1}:${error
1325          .location.first_column + 1}`;
1326        detail = `${error.message} in ${location}`;
1327        stack = 'SyntaxError: ' + error.message + '\n' + 'at ' + location;
1328      } else if (
1329        error.less &&
1330        error.filename &&
1331        error.column != null &&
1332        error.line != null
1333      ) {
1334        location = `${error.filename}:${error.line}:${error.column}`;
1335        detail = `${error.message} in ${location}`;
1336        stack = 'LessError: ' + error.message + '\n' + 'at ' + location;
1337      } else {
1338        detail = error.message;
1339        stack = error.stack || error;
1340      }
1341  
1342      this.notificationManager.addFatalError(message, {
1343        stack,
1344        detail,
1345        packageName: this.name,
1346        dismissable: true
1347      });
1348    }
1349  };
1350  
1351  class SettingsFile {
1352    static load(path, callback) {
1353      CSON.readFile(path, (error, properties = {}) => {
1354        if (error) {
1355          callback(error);
1356        } else {
1357          callback(null, new SettingsFile(path, properties));
1358        }
1359      });
1360    }
1361  
1362    constructor(path, properties) {
1363      this.path = path;
1364      this.properties = properties;
1365    }
1366  
1367    activate(config) {
1368      for (let selector in this.properties) {
1369        config.set(null, this.properties[selector], {
1370          scopeSelector: selector,
1371          source: this.path
1372        });
1373      }
1374    }
1375  
1376    deactivate(config) {
1377      for (let selector in this.properties) {
1378        config.unset(null, { scopeSelector: selector, source: this.path });
1379      }
1380    }
1381  }