Mailspring/src/package.coffee
2016-05-06 11:54:55 -07:00

406 lines
13 KiB
CoffeeScript

path = require 'path'
_ = require 'underscore'
async = require 'async'
fs = require 'fs-plus'
EmitterMixin = require('emissary').Emitter
{Emitter, CompositeDisposable} = require 'event-kit'
Q = require 'q'
{deprecate} = require 'grim'
ModuleCache = require './module-cache'
TaskRegistry = require('./task-registry').default
DatabaseObjectRegistry = require('./database-object-registry').default
try
packagesCache = require('../package.json')?._N1Packages ? {}
catch error
packagesCache = {}
# Loads and activates a package's main module and resources such as
# stylesheets, keymaps, and menus.
module.exports =
class Package
EmitterMixin.includeInto(this)
@isBundledPackagePath: (packagePath) ->
if NylasEnv.packages.devMode
return false unless NylasEnv.packages.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
@resourcePathWithTrailingSlash ?= "#{NylasEnv.packages.resourcePath}#{path.sep}"
packagePath?.startsWith(@resourcePathWithTrailingSlash)
@loadMetadata: (packagePath, ignoreErrors=false) ->
packageName = path.basename(packagePath)
if @isBundledPackagePath(packagePath)
metadata = packagesCache[packageName]?.metadata
unless metadata?
metadataPath = fs.resolve(path.join(packagePath, 'package.json'))
if fs.existsSync(metadataPath)
try
metadata = JSON.parse(fs.readFileSync(metadataPath))
catch error
throw error unless ignoreErrors
metadata ?= {}
metadata.name = packageName
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
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)
if @metadata.appId
if _.isString @metadata.appId
@pluginAppId = @metadata.appId ? null
else if _.isObject @metadata.appId
@pluginAppId = @metadata.appId[NylasEnv.config.get('env')] ? null
else
@pluginAppId = null
else
@pluginAppId = null
@displayName = @metadata?.displayName || @name
ModuleCache.add(@path, @metadata)
@reset()
@declaresNewDatabaseObjects = false
# TODO FIXME: Use a unique pluginID instead of just the "name"
# This needs to be included here to prevent a circular dependency error
pluginId: -> return @pluginAppId ? @name
###
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
###
Section: Instance Methods
###
enable: ->
NylasEnv.config.removeAtKeyPath('core.disabledPackages', @name)
disable: ->
NylasEnv.config.pushAtKeyPath('core.disabledPackages', @name)
isTheme: ->
@metadata?.theme?
measure: (key, fn) ->
startTime = Date.now()
value = fn()
@[key] = Date.now() - startTime
value
getType: -> 'nylas'
getStyleSheetPriority: -> 0
load: ->
@measure 'loadTime', =>
try
@declaresNewDatabaseObjects = false
@loadKeymaps()
@loadMenus()
@loadStylesheets()
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
_.each constructors, (constructor) ->
constructorFactory = -> constructor
DatabaseObjectRegistry.register(constructor.name, constructorFactory)
registerTaskConstructors: (constructors=[]) ->
_.each constructors, (constructor) ->
constructorFactory = -> constructor
TaskRegistry.register(constructor.name, constructorFactory)
reset: ->
@stylesheets = []
@keymaps = []
@menus = []
activate: ->
unless @activationDeferred?
@activationDeferred = Q.defer()
@measure 'activateTime', =>
@activateResources()
@activateNow()
Q.all([@activationDeferred.promise])
activateNow: ->
try
@activateConfig()
@activateStylesheets()
if @requireMainModule()
localState = NylasEnv.packages.getPackageState(@name) ? {}
@mainModule.activate(localState)
@mainActivated = true
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'
NylasEnv.config.setSchema @name, {type: 'object', properties: @mainModule.config}
else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object'
NylasEnv.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
context = undefined
@stylesheetDisposables.add(NylasEnv.styles.addStyleSheet(source, {sourcePath, priority, context}))
@stylesheetsActivated = true
activateResources: ->
@activationDisposables = new CompositeDisposable
@activationDisposables.add(NylasEnv.keymaps.loadKeymap(keymapPath, map)) for [keymapPath, map] in @keymaps
@activationDisposables.add(NylasEnv.menu.add(map['menu'])) for [menuPath, map] in @menus when map['menu']?
loadKeymaps: ->
try
if @bundledPackage and packagesCache[@name]?
@keymaps = (["#{NylasEnv.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
else
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, JSON.parse(fs.readFileSync(keymapPath)) ? {}]
catch e
console.error "Error reading keymaps for package '#{@name}': #{e.message}", e.stack
loadMenus: ->
try
if @bundledPackage and packagesCache[@name]?
@menus = (["#{NylasEnv.packages.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of packagesCache[@name].menus)
else
@menus = @getMenuPaths().map (menuPath) -> [menuPath, JSON.parse(fs.readFileSync(menuPath)) ? {}]
catch e
console.error "Error reading menus for package '#{@name}': #{e.message}", e.stack
getKeymapPaths: ->
keymapsDirPath = path.join(@path, 'keymaps')
if @metadata.keymaps
@metadata.keymaps.map (name) -> fs.resolve(keymapsDirPath, name, ['json', ''])
else
fs.listSync(keymapsDirPath, ['json'])
getMenuPaths: ->
menusDirPath = path.join(@path, 'menus')
if @metadata.menus
@metadata.menus.map (name) -> fs.resolve(menusDirPath, name, ['json', ''])
else
fs.listSync(menusDirPath, ['json'])
loadStylesheets: ->
@stylesheets = @getStylesheetPaths().map (stylesheetPath) ->
[stylesheetPath, NylasEnv.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 '.'
serialize: ->
if @mainActivated
try
@mainModule?.serialize?()
catch e
console.error "Error serializing package '#{@name}'", e.stack
deactivate: ->
@activationDeferred?.reject()
@activationDeferred = null
@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: ->
@stylesheetDisposables?.dispose()
@activationDisposables?.dispose()
@stylesheetsActivated = 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 = "#{NylasEnv.packages.resourcePath}#{path.sep}#{packagesCache[@name].main}"
@mainModulePath = fs.resolveExtension(@mainModulePath, ["", Object.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, ["", Object.keys(require.extensions)...])
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 NylasEnv.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 N1?
#
# Incompatible packages cannot be activated. This will include packages
# installed to ~/.nylas/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(NylasEnv.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