mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-24 09:16:07 +08:00
e303705b45
Summary: Features: - ThreadListParticipants ignores drafts when computing participants, renders "Draft" label, pending design - Put the WorkspaceStore in every window—means they all get toolbars and custom gumdrop icons on Mac OS X Bug Fixes: - Never display notifications for email the user just sent - Fix obscure issue with DatabaseView trying to update metadata on items it froze. This resolves issue with names remaining bold after marking as read, drafts not appearing in message list immediately. - When you pop out a draft, save it first and *wait* for the commit() promise to succeed. - If you scroll very fast, you node.contentWindow can be null in eventedIframe Other: Make it OK to re-register the same component Make it possible to unregister a hot window Break the Sheet Toolbar out into it's own file to make things manageable Replace `package.windowPropsReceived` with a store-style model where anyone can listen for changes to `windowProps` When I put the WorkspaceStore in every window, I ran into a problem because the package was no longer rendering an instance of the Composer, it was declaring a root sheet with a composer in it. This meant that it was actually a React component that needed to listen to window props, not the package itself. `atom` is already an event emitter, so I added a `onWindowPropsReceived` hook so that components can listen to window props as if they were listening to a store. I think this might be more flexible than only broadcasting the props change event to packages. Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1592
537 lines
18 KiB
CoffeeScript
537 lines
18 KiB
CoffeeScript
path = require 'path'
|
|
|
|
_ = require 'underscore'
|
|
async = require 'async'
|
|
CSON = require 'season'
|
|
fs = require 'fs-plus'
|
|
EmitterMixin = require('emissary').Emitter
|
|
{Emitter, CompositeDisposable} = require 'event-kit'
|
|
Q = require 'q'
|
|
{deprecate} = require 'grim'
|
|
|
|
ModuleCache = require './module-cache'
|
|
ScopedProperties = require './scoped-properties'
|
|
|
|
try
|
|
packagesCache = require('../package.json')?._atomPackages ? {}
|
|
catch error
|
|
packagesCache = {}
|
|
|
|
# Loads and activates a package's main module and resources such as
|
|
# stylesheets, keymaps, grammar, editor properties, and menus.
|
|
module.exports =
|
|
class Package
|
|
EmitterMixin.includeInto(this)
|
|
|
|
@isBundledPackagePath: (packagePath) ->
|
|
if atom.packages.devMode
|
|
return false unless atom.packages.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
|
|
|
|
@resourcePathWithTrailingSlash ?= "#{atom.packages.resourcePath}#{path.sep}"
|
|
packagePath?.startsWith(@resourcePathWithTrailingSlash)
|
|
|
|
@loadMetadata: (packagePath, ignoreErrors=false) ->
|
|
packageName = path.basename(packagePath)
|
|
if @isBundledPackagePath(packagePath)
|
|
metadata = packagesCache[packageName]?.metadata
|
|
unless metadata?
|
|
if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
|
|
try
|
|
metadata = CSON.readFileSync(metadataPath)
|
|
catch error
|
|
throw error unless ignoreErrors
|
|
metadata ?= {}
|
|
metadata.name = packageName
|
|
|
|
if metadata.stylesheetMain?
|
|
deprecate("Use the `mainStyleSheet` key instead of `stylesheetMain` in the `package.json` of `#{packageName}`", {packageName})
|
|
metadata.mainStyleSheet = metadata.stylesheetMain
|
|
|
|
if metadata.stylesheets?
|
|
deprecate("Use the `styleSheets` key instead of `stylesheets` in the `package.json` of `#{packageName}`", {packageName})
|
|
metadata.styleSheets = metadata.stylesheets
|
|
|
|
metadata
|
|
|
|
keymaps: null
|
|
menus: null
|
|
stylesheets: null
|
|
stylesheetDisposables: null
|
|
grammars: null
|
|
settings: null
|
|
mainModulePath: null
|
|
resolvedMainModulePath: false
|
|
mainModule: null
|
|
|
|
###
|
|
Section: Construction
|
|
###
|
|
|
|
constructor: (@path, @metadata) ->
|
|
@emitter = new Emitter
|
|
@metadata ?= Package.loadMetadata(@path)
|
|
@bundledPackage = Package.isBundledPackagePath(@path)
|
|
@name = @metadata?.name ? path.basename(@path)
|
|
ModuleCache.add(@path, @metadata)
|
|
@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) ->
|
|
@emitter.on 'did-deactivate', callback
|
|
|
|
on: (eventName) ->
|
|
switch eventName
|
|
when 'deactivated'
|
|
deprecate 'Use Package::onDidDeactivate instead'
|
|
else
|
|
deprecate 'Package::on is deprecated. Use event subscription methods instead.'
|
|
EmitterMixin::on.apply(this, arguments)
|
|
|
|
###
|
|
Section: Instance Methods
|
|
###
|
|
|
|
enable: ->
|
|
atom.config.removeAtKeyPath('core.disabledPackages', @name)
|
|
|
|
disable: ->
|
|
atom.config.pushAtKeyPath('core.disabledPackages', @name)
|
|
|
|
isTheme: ->
|
|
@metadata?.theme?
|
|
|
|
measure: (key, fn) ->
|
|
startTime = Date.now()
|
|
value = fn()
|
|
@[key] = Date.now() - startTime
|
|
value
|
|
|
|
getType: -> 'atom'
|
|
|
|
getStyleSheetPriority: -> 0
|
|
|
|
load: ->
|
|
@measure 'loadTime', =>
|
|
try
|
|
@loadKeymaps()
|
|
@loadMenus()
|
|
@loadStylesheets()
|
|
@settingsPromise = @loadSettings()
|
|
@requireMainModule() unless @hasActivationCommands()
|
|
|
|
catch error
|
|
console.warn "Failed to load package named '#{@name}'"
|
|
console.warn error.stack ? error
|
|
console.error(error.message, error)
|
|
this
|
|
|
|
reset: ->
|
|
@stylesheets = []
|
|
@keymaps = []
|
|
@menus = []
|
|
@grammars = []
|
|
@settings = []
|
|
|
|
activate: ->
|
|
@grammarsPromise ?= @loadGrammars()
|
|
|
|
unless @activationDeferred?
|
|
@activationDeferred = Q.defer()
|
|
@measure 'activateTime', =>
|
|
@activateResources()
|
|
if @hasActivationCommands()
|
|
@subscribeToActivationCommands()
|
|
else
|
|
@activateNow()
|
|
|
|
Q.all([@grammarsPromise, @settingsPromise, @activationDeferred.promise])
|
|
|
|
activateNow: ->
|
|
try
|
|
@activateConfig()
|
|
@activateStylesheets()
|
|
if @requireMainModule()
|
|
@mainModule.activate(atom.packages.getPackageState(@name) ? {}, path.resolve(@path))
|
|
@mainActivated = true
|
|
@activateServices()
|
|
catch e
|
|
console.log e.message
|
|
console.log e.stack
|
|
console.warn "Failed to activate package named '#{@name}'", e.stack
|
|
|
|
@activationDeferred?.resolve()
|
|
|
|
activateConfig: ->
|
|
return if @configActivated
|
|
|
|
@requireMainModule()
|
|
if @mainModule?
|
|
if @mainModule.config? and typeof @mainModule.config is 'object'
|
|
atom.config.setSchema @name, {type: 'object', properties: @mainModule.config}
|
|
else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object'
|
|
deprecate """Use a config schema instead. See the configuration section
|
|
of https://atom.io/docs/latest/creating-a-package and
|
|
https://atom.io/docs/api/latest/Config for more details"""
|
|
atom.config.setDefaults(@name, @mainModule.configDefaults)
|
|
@mainModule.activateConfig?()
|
|
@configActivated = true
|
|
|
|
activateStylesheets: ->
|
|
return if @stylesheetsActivated
|
|
|
|
@stylesheetDisposables = new CompositeDisposable
|
|
|
|
priority = @getStyleSheetPriority()
|
|
for [sourcePath, source] in @stylesheets
|
|
if match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./)
|
|
context = match[1]
|
|
else if @metadata.theme is 'syntax'
|
|
context = 'atom-text-editor'
|
|
else
|
|
context = undefined
|
|
|
|
@stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, priority, context}))
|
|
@stylesheetsActivated = true
|
|
|
|
activateResources: ->
|
|
@activationDisposables = new CompositeDisposable
|
|
@activationDisposables.add(atom.keymaps.add(keymapPath, map)) for [keymapPath, map] in @keymaps
|
|
@activationDisposables.add(atom.menu.add(map['menu'])) for [menuPath, map] in @menus when map['menu']?
|
|
|
|
unless @grammarsActivated
|
|
grammar.activate() for grammar in @grammars
|
|
@grammarsActivated = true
|
|
|
|
settings.activate() for settings in @settings
|
|
@settingsActivated = true
|
|
|
|
activateServices: ->
|
|
for name, {versions} of @metadata.providedServices
|
|
for version, methodName of versions
|
|
@activationDisposables.add atom.packages.serviceHub.provide(name, version, @mainModule[methodName]())
|
|
|
|
for name, {versions} of @metadata.consumedServices
|
|
for version, methodName of versions
|
|
@activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
|
|
|
|
loadKeymaps: ->
|
|
if @bundledPackage and packagesCache[@name]?
|
|
@keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
|
|
else
|
|
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}]
|
|
|
|
loadMenus: ->
|
|
if @bundledPackage and packagesCache[@name]?
|
|
@menus = (["#{atom.packages.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of packagesCache[@name].menus)
|
|
else
|
|
@menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}]
|
|
|
|
getKeymapPaths: ->
|
|
keymapsDirPath = path.join(@path, 'keymaps')
|
|
if @metadata.keymaps
|
|
@metadata.keymaps.map (name) -> fs.resolve(keymapsDirPath, name, ['json', 'cson', ''])
|
|
else
|
|
fs.listSync(keymapsDirPath, ['cson', 'json'])
|
|
|
|
getMenuPaths: ->
|
|
menusDirPath = path.join(@path, 'menus')
|
|
if @metadata.menus
|
|
@metadata.menus.map (name) -> fs.resolve(menusDirPath, name, ['json', 'cson', ''])
|
|
else
|
|
fs.listSync(menusDirPath, ['cson', 'json'])
|
|
|
|
loadStylesheets: ->
|
|
@stylesheets = @getStylesheetPaths().map (stylesheetPath) ->
|
|
[stylesheetPath, atom.themes.loadStylesheet(stylesheetPath, true)]
|
|
|
|
getStylesheetsPath: ->
|
|
if fs.isDirectorySync(path.join(@path, 'stylesheets'))
|
|
deprecate("Store package style sheets in the `styles/` directory instead of `stylesheets/` in the `#{@name}` package", packageName: @name)
|
|
path.join(@path, 'stylesheets')
|
|
else
|
|
path.join(@path, 'styles')
|
|
|
|
getStylesheetPaths: ->
|
|
stylesheetDirPath = @getStylesheetsPath()
|
|
if @metadata.mainStyleSheet
|
|
[fs.resolve(@path, @metadata.mainStyleSheet)]
|
|
else if @metadata.styleSheets
|
|
@metadata.styleSheets.map (name) -> fs.resolve(stylesheetDirPath, name, ['css', 'less', ''])
|
|
else if indexStylesheet = fs.resolve(@path, 'index', ['css', 'less'])
|
|
[indexStylesheet]
|
|
else
|
|
_.filter fs.listSync(stylesheetDirPath, ['css', 'less']), (file) ->
|
|
path.basename(file)[0] isnt '.'
|
|
|
|
loadGrammarsSync: ->
|
|
return if @grammarsLoaded
|
|
|
|
grammarsDirPath = path.join(@path, 'grammars')
|
|
grammarPaths = fs.listSync(grammarsDirPath, ['json', 'cson'])
|
|
for grammarPath in grammarPaths
|
|
try
|
|
grammar = atom.grammars.readGrammarSync(grammarPath)
|
|
grammar.packageName = @name
|
|
@grammars.push(grammar)
|
|
grammar.activate()
|
|
catch error
|
|
console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
|
|
|
|
@grammarsLoaded = true
|
|
@grammarsActivated = true
|
|
|
|
loadGrammars: ->
|
|
return Q() if @grammarsLoaded
|
|
|
|
loadGrammar = (grammarPath, callback) =>
|
|
atom.grammars.readGrammar grammarPath, (error, grammar) =>
|
|
if error?
|
|
console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
|
|
else
|
|
grammar.packageName = @name
|
|
@grammars.push(grammar)
|
|
grammar.activate() if @grammarsActivated
|
|
callback()
|
|
|
|
deferred = Q.defer()
|
|
grammarsDirPath = path.join(@path, 'grammars')
|
|
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
|
|
async.each grammarPaths, loadGrammar, -> deferred.resolve()
|
|
deferred.promise
|
|
|
|
loadSettings: ->
|
|
@settings = []
|
|
|
|
loadSettingsFile = (settingsPath, callback) =>
|
|
ScopedProperties.load settingsPath, (error, settings) =>
|
|
if error?
|
|
console.warn("Failed to load package settings: #{settingsPath}", error.stack ? error)
|
|
else
|
|
@settings.push(settings)
|
|
settings.activate() if @settingsActivated
|
|
callback()
|
|
|
|
deferred = Q.defer()
|
|
|
|
if fs.isDirectorySync(path.join(@path, 'scoped-properties'))
|
|
settingsDirPath = path.join(@path, 'scoped-properties')
|
|
deprecate("Store package settings files in the `settings/` directory instead of `scoped-properties/`", packageName: @name)
|
|
else
|
|
settingsDirPath = path.join(@path, 'settings')
|
|
|
|
fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
|
|
async.each settingsPaths, loadSettingsFile, -> deferred.resolve()
|
|
deferred.promise
|
|
|
|
serialize: ->
|
|
if @mainActivated
|
|
try
|
|
@mainModule?.serialize?()
|
|
catch e
|
|
console.error "Error serializing package '#{@name}'", e.stack
|
|
|
|
deactivate: ->
|
|
@activationDeferred?.reject()
|
|
@activationDeferred = null
|
|
@activationCommandSubscriptions?.dispose()
|
|
@deactivateResources()
|
|
@deactivateConfig()
|
|
if @mainActivated
|
|
try
|
|
@mainModule?.deactivate?()
|
|
catch e
|
|
console.error "Error deactivating package '#{@name}'", e.stack
|
|
@emit 'deactivated'
|
|
@emitter.emit 'did-deactivate'
|
|
|
|
deactivateConfig: ->
|
|
@mainModule?.deactivateConfig?()
|
|
@configActivated = false
|
|
|
|
deactivateResources: ->
|
|
grammar.deactivate() for grammar in @grammars
|
|
settings.deactivate() for settings in @settings
|
|
@stylesheetDisposables?.dispose()
|
|
@activationDisposables?.dispose()
|
|
@stylesheetsActivated = false
|
|
@grammarsActivated = false
|
|
@settingsActivated = false
|
|
|
|
reloadStylesheets: ->
|
|
oldSheets = _.clone(@stylesheets)
|
|
@loadStylesheets()
|
|
@stylesheetDisposables?.dispose()
|
|
@stylesheetDisposables = new CompositeDisposable
|
|
@stylesheetsActivated = false
|
|
@activateStylesheets()
|
|
|
|
requireMainModule: ->
|
|
return @mainModule if @mainModule?
|
|
unless @isCompatible()
|
|
console.warn """
|
|
Failed to require the main module of '#{@name}' because it requires an incompatible native module.
|
|
Run `apm rebuild` in the package directory to resolve.
|
|
"""
|
|
return
|
|
mainModulePath = @getMainModulePath()
|
|
@mainModule = require(mainModulePath) if fs.isFileSync(mainModulePath)
|
|
return @mainModule
|
|
|
|
getMainModulePath: ->
|
|
return @mainModulePath if @resolvedMainModulePath
|
|
@resolvedMainModulePath = true
|
|
|
|
if @bundledPackage and packagesCache[@name]?
|
|
if packagesCache[@name].main
|
|
@mainModulePath = "#{atom.packages.resourcePath}#{path.sep}#{packagesCache[@name].main}"
|
|
@mainModulePath = fs.resolveExtension(@mainModulePath, ["", _.keys(require.extensions)...])
|
|
else
|
|
@mainModulePath = null
|
|
else
|
|
mainModulePath =
|
|
if @metadata.main
|
|
path.join(@path, @metadata.main)
|
|
else
|
|
path.join(@path, 'index')
|
|
@mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...])
|
|
|
|
hasActivationCommands: ->
|
|
for selector, commands of @getActivationCommands()
|
|
return true if commands.length > 0
|
|
false
|
|
|
|
subscribeToActivationCommands: ->
|
|
@activationCommandSubscriptions = new CompositeDisposable
|
|
for selector, commands of @getActivationCommands()
|
|
for command in commands
|
|
do (selector, command) =>
|
|
# Add dummy command so it appears in menu.
|
|
# The real command will be registered on package activation
|
|
@activationCommandSubscriptions.add atom.commands.add selector, command, ->
|
|
@activationCommandSubscriptions.add atom.commands.onWillDispatch (event) =>
|
|
return unless event.type is command
|
|
currentTarget = event.target
|
|
while currentTarget
|
|
if currentTarget.webkitMatchesSelector(selector)
|
|
@activationCommandSubscriptions.dispose()
|
|
@activateNow()
|
|
break
|
|
currentTarget = currentTarget.parentElement
|
|
|
|
getActivationCommands: ->
|
|
return @activationCommands if @activationCommands?
|
|
|
|
@activationCommands = {}
|
|
|
|
if @metadata.activationCommands?
|
|
for selector, commands of @metadata.activationCommands
|
|
@activationCommands[selector] ?= []
|
|
if _.isString(commands)
|
|
@activationCommands[selector].push(commands)
|
|
else if _.isArray(commands)
|
|
@activationCommands[selector].push(commands...)
|
|
|
|
if @metadata.activationEvents?
|
|
deprecate """
|
|
Use `activationCommands` instead of `activationEvents` in your package.json
|
|
Commands should be grouped by selector as follows:
|
|
```json
|
|
"activationCommands": {
|
|
"atom-workspace": ["foo:bar", "foo:baz"],
|
|
"atom-text-editor": ["foo:quux"]
|
|
}
|
|
```
|
|
"""
|
|
if _.isArray(@metadata.activationEvents)
|
|
for eventName in @metadata.activationEvents
|
|
@activationCommands['atom-workspace'] ?= []
|
|
@activationCommands['atom-workspace'].push(eventName)
|
|
else if _.isString(@metadata.activationEvents)
|
|
eventName = @metadata.activationEvents
|
|
@activationCommands['atom-workspace'] ?= []
|
|
@activationCommands['atom-workspace'].push(eventName)
|
|
else
|
|
for eventName, selector of @metadata.activationEvents
|
|
selector ?= 'atom-workspace'
|
|
@activationCommands[selector] ?= []
|
|
@activationCommands[selector].push(eventName)
|
|
|
|
@activationCommands
|
|
|
|
# Does the given module path contain native code?
|
|
isNativeModule: (modulePath) ->
|
|
try
|
|
fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node']).length > 0
|
|
catch error
|
|
false
|
|
|
|
# Get an array of all the native modules that this package depends on.
|
|
# This will recurse through all dependencies.
|
|
getNativeModuleDependencyPaths: ->
|
|
nativeModulePaths = []
|
|
|
|
traversePath = (nodeModulesPath) =>
|
|
try
|
|
for modulePath in fs.listSync(nodeModulesPath)
|
|
nativeModulePaths.push(modulePath) if @isNativeModule(modulePath)
|
|
traversePath(path.join(modulePath, 'node_modules'))
|
|
|
|
traversePath(path.join(@path, 'node_modules'))
|
|
nativeModulePaths
|
|
|
|
# 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: ->
|
|
localStorageKey = "installed-packages:#{@name}:#{@metadata.version}"
|
|
unless atom.inDevMode()
|
|
try
|
|
{incompatibleNativeModules} = JSON.parse(global.localStorage.getItem(localStorageKey)) ? {}
|
|
return incompatibleNativeModules if incompatibleNativeModules?
|
|
|
|
incompatibleNativeModules = []
|
|
for nativeModulePath in @getNativeModuleDependencyPaths()
|
|
try
|
|
require(nativeModulePath)
|
|
catch error
|
|
try
|
|
version = require("#{nativeModulePath}/package.json").version
|
|
incompatibleNativeModules.push
|
|
path: nativeModulePath
|
|
name: path.basename(nativeModulePath)
|
|
version: version
|
|
error: error.message
|
|
|
|
global.localStorage.setItem(localStorageKey, JSON.stringify({incompatibleNativeModules}))
|
|
incompatibleNativeModules
|
|
|
|
# Public: Is this package compatible with this version of Atom?
|
|
#
|
|
# Incompatible packages cannot be activated. This will include packages
|
|
# installed to ~/.atom/packages that were built against node 0.11.10 but
|
|
# now need to be upgrade to node 0.11.13.
|
|
#
|
|
# Returns a {Boolean}, true if compatible, false if incompatible.
|
|
isCompatible: ->
|
|
return @compatible if @compatible?
|
|
|
|
if @path.indexOf(path.join(atom.packages.resourcePath, 'node_modules') + path.sep) is 0
|
|
# Bundled packages are always considered compatible
|
|
@compatible = true
|
|
else if packageMain = @getMainModulePath()
|
|
@incompatibleModules = @getIncompatibleNativeModules()
|
|
@compatible = @incompatibleModules.length is 0
|
|
else
|
|
@compatible = true
|