mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-11 18:32:20 +08:00
d2b979e716
Summary: Adding serialzable registry Added DatabaseObjectRegistry rename modelReviver to deserializeObject Consolidate deserizlie Get rid of model methods from Utils DatabaseRegistry change notifications Logic to throttle database refresh requests Fixes in nylas-exports Silent model setup Continue to resolving the database setup phase for non main windows. A packages `activate` method does not actually get called until the DatabaseStore says that it's ready. This is necessary to ensure that a package that introduces database changes has those schema changes take hold before the activate happens. However, in windows like the `onboarding` window that do not depend on a database at all, there is no setup that is run and the promise use to never resolve thereby making the packages never activate. In this case, any external windows will go ahead and let their packages activate. Check subclass instead of instance! Use the correct types for "messages" and "drafts" Move Salesforce models Proper references to Model and Attributes Convert Salesforce stores to NylasStores and fix paths Move database setup to DB Test Plan: todo Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D1899
555 lines
19 KiB
CoffeeScript
555 lines
19 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'
|
|
|
|
TaskRegistry = require './task-registry'
|
|
DatabaseObjectRegistry = require './database-object-registry'
|
|
|
|
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()
|
|
@declaresNewDatabaseObjects = false
|
|
|
|
###
|
|
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
|
|
@declaresNewDatabaseObjects = false
|
|
@loadKeymaps()
|
|
@loadMenus()
|
|
@loadStylesheets()
|
|
@settingsPromise = @loadSettings()
|
|
if not @hasActivationCommands()
|
|
mainModule = @requireMainModule()
|
|
return unless mainModule
|
|
@registerModelConstructors(mainModule.modelConstructors)
|
|
@registerTaskConstructors(mainModule.taskConstructors)
|
|
|
|
catch error
|
|
console.warn "Failed to load package named '#{@name}'"
|
|
console.warn error.stack ? error
|
|
console.error(error.message, error)
|
|
this
|
|
|
|
registerModelConstructors: (constructors=[]) ->
|
|
if constructors.length > 0
|
|
@declaresNewDatabaseObjects = true
|
|
for constructor in constructors
|
|
DatabaseObjectRegistry.register(constructor)
|
|
|
|
registerTaskConstructors: (constructors=[]) ->
|
|
for constructor in constructors
|
|
TaskRegistry.register(constructor)
|
|
|
|
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
|