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 }