mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-27 02:23:28 +08:00
406 lines
13 KiB
CoffeeScript
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
|