mirror of
https://github.com/atom/atom.git
synced 2026-01-23 13:58:08 -05:00
1108 lines
36 KiB
JavaScript
1108 lines
36 KiB
JavaScript
const path = require('path')
|
|
const async = require('async')
|
|
const CSON = require('season')
|
|
const fs = require('fs-plus')
|
|
const {Emitter, CompositeDisposable} = require('event-kit')
|
|
const dedent = require('dedent')
|
|
|
|
const CompileCache = require('./compile-cache')
|
|
const ModuleCache = require('./module-cache')
|
|
const ScopedProperties = require('./scoped-properties')
|
|
const BufferedProcess = require('./buffered-process')
|
|
|
|
// Extended: Loads and activates a package's main module and resources such as
|
|
// stylesheets, keymaps, grammar, editor properties, and menus.
|
|
module.exports =
|
|
class Package {
|
|
/*
|
|
Section: Construction
|
|
*/
|
|
|
|
constructor (params) {
|
|
this.config = params.config
|
|
this.packageManager = params.packageManager
|
|
this.styleManager = params.styleManager
|
|
this.commandRegistry = params.commandRegistry
|
|
this.keymapManager = params.keymapManager
|
|
this.notificationManager = params.notificationManager
|
|
this.grammarRegistry = params.grammarRegistry
|
|
this.themeManager = params.themeManager
|
|
this.menuManager = params.menuManager
|
|
this.contextMenuManager = params.contextMenuManager
|
|
this.deserializerManager = params.deserializerManager
|
|
this.viewRegistry = params.viewRegistry
|
|
this.emitter = new Emitter()
|
|
|
|
this.mainModule = null
|
|
this.path = params.path
|
|
this.preloadedPackage = params.preloadedPackage
|
|
this.metadata =
|
|
params.metadata ||
|
|
this.packageManager.loadPackageMetadata(this.path)
|
|
this.bundledPackage = params.bundledPackage != null
|
|
? params.bundledPackage
|
|
: this.packageManager.isBundledPackagePath(this.path)
|
|
this.name =
|
|
params.name ||
|
|
(this.metadata && this.metadata.name) ||
|
|
path.basename(this.path)
|
|
this.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) {
|
|
return this.emitter.on('did-deactivate', callback)
|
|
}
|
|
|
|
/*
|
|
Section: Instance Methods
|
|
*/
|
|
|
|
enable () {
|
|
return this.config.removeAtKeyPath('core.disabledPackages', this.name)
|
|
}
|
|
|
|
disable () {
|
|
return this.config.pushAtKeyPath('core.disabledPackages', this.name)
|
|
}
|
|
|
|
isTheme () {
|
|
return this.metadata && this.metadata.theme
|
|
}
|
|
|
|
measure (key, fn) {
|
|
const startTime = Date.now()
|
|
const value = fn()
|
|
this[key] = Date.now() - startTime
|
|
return value
|
|
}
|
|
|
|
getType () { return 'atom' }
|
|
|
|
getStyleSheetPriority () { return 0 }
|
|
|
|
preload () {
|
|
this.loadKeymaps()
|
|
this.loadMenus()
|
|
this.registerDeserializerMethods()
|
|
this.activateCoreStartupServices()
|
|
this.registerURIHandler()
|
|
this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata()
|
|
this.requireMainModule()
|
|
this.settingsPromise = this.loadSettings()
|
|
|
|
this.activationDisposables = new CompositeDisposable()
|
|
this.activateKeymaps()
|
|
this.activateMenus()
|
|
for (let settings of this.settings) {
|
|
settings.activate()
|
|
}
|
|
this.settingsActivated = true
|
|
}
|
|
|
|
finishLoading () {
|
|
this.measure('loadTime', () => {
|
|
this.path = path.join(this.packageManager.resourcePath, this.path)
|
|
ModuleCache.add(this.path, this.metadata)
|
|
|
|
this.loadStylesheets()
|
|
// Unfortunately some packages are accessing `@mainModulePath`, so we need
|
|
// to compute that variable eagerly also for preloaded packages.
|
|
this.getMainModulePath()
|
|
})
|
|
}
|
|
|
|
load () {
|
|
this.measure('loadTime', () => {
|
|
try {
|
|
ModuleCache.add(this.path, this.metadata)
|
|
|
|
this.loadKeymaps()
|
|
this.loadMenus()
|
|
this.loadStylesheets()
|
|
this.registerDeserializerMethods()
|
|
this.activateCoreStartupServices()
|
|
this.registerURIHandler()
|
|
this.registerTranspilerConfig()
|
|
this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata()
|
|
this.settingsPromise = this.loadSettings()
|
|
if (this.shouldRequireMainModuleOnLoad() && (this.mainModule == null)) {
|
|
this.requireMainModule()
|
|
}
|
|
} catch (error) {
|
|
this.handleError(`Failed to load the ${this.name} package`, error)
|
|
}
|
|
})
|
|
return this
|
|
}
|
|
|
|
unload () {
|
|
this.unregisterTranspilerConfig()
|
|
}
|
|
|
|
shouldRequireMainModuleOnLoad () {
|
|
return !(
|
|
this.metadata.deserializers ||
|
|
this.metadata.viewProviders ||
|
|
this.metadata.configSchema ||
|
|
this.activationShouldBeDeferred() ||
|
|
localStorage.getItem(this.getCanDeferMainModuleRequireStorageKey()) === 'true'
|
|
)
|
|
}
|
|
|
|
reset () {
|
|
this.stylesheets = []
|
|
this.keymaps = []
|
|
this.menus = []
|
|
this.grammars = []
|
|
this.settings = []
|
|
this.mainInitialized = false
|
|
this.mainActivated = false
|
|
}
|
|
|
|
initializeIfNeeded () {
|
|
if (this.mainInitialized) return
|
|
this.measure('initializeTime', () => {
|
|
try {
|
|
// The main module's `initialize()` method is guaranteed to be called
|
|
// before its `activate()`. This gives you a chance to handle the
|
|
// serialized package state before the package's derserializers and view
|
|
// providers are used.
|
|
if (!this.mainModule) this.requireMainModule()
|
|
if (typeof this.mainModule.initialize === 'function') {
|
|
this.mainModule.initialize(this.packageManager.getPackageState(this.name) || {})
|
|
}
|
|
this.mainInitialized = true
|
|
} catch (error) {
|
|
this.handleError(`Failed to initialize the ${this.name} package`, error)
|
|
}
|
|
})
|
|
}
|
|
|
|
activate () {
|
|
if (!this.grammarsPromise) this.grammarsPromise = this.loadGrammars()
|
|
if (!this.activationPromise) {
|
|
this.activationPromise = new Promise((resolve, reject) => {
|
|
this.resolveActivationPromise = resolve
|
|
this.measure('activateTime', () => {
|
|
try {
|
|
this.activateResources()
|
|
if (this.activationShouldBeDeferred()) {
|
|
return this.subscribeToDeferredActivation()
|
|
} else {
|
|
return this.activateNow()
|
|
}
|
|
} catch (error) {
|
|
return this.handleError(`Failed to activate the ${this.name} package`, error)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
return Promise.all([this.grammarsPromise, this.settingsPromise, this.activationPromise])
|
|
}
|
|
|
|
activateNow () {
|
|
try {
|
|
if (!this.mainModule) this.requireMainModule()
|
|
this.configSchemaRegisteredOnActivate = this.registerConfigSchemaFromMainModule()
|
|
this.registerViewProviders()
|
|
this.activateStylesheets()
|
|
if (this.mainModule && !this.mainActivated) {
|
|
this.initializeIfNeeded()
|
|
if (typeof this.mainModule.activateConfig === 'function') {
|
|
this.mainModule.activateConfig()
|
|
}
|
|
if (typeof this.mainModule.activate === 'function') {
|
|
this.mainModule.activate(this.packageManager.getPackageState(this.name) || {})
|
|
}
|
|
this.mainActivated = true
|
|
this.activateServices()
|
|
}
|
|
if (this.activationCommandSubscriptions) this.activationCommandSubscriptions.dispose()
|
|
if (this.activationHookSubscriptions) this.activationHookSubscriptions.dispose()
|
|
} catch (error) {
|
|
this.handleError(`Failed to activate the ${this.name} package`, error)
|
|
}
|
|
|
|
if (typeof this.resolveActivationPromise === 'function') this.resolveActivationPromise()
|
|
}
|
|
|
|
registerConfigSchemaFromMetadata () {
|
|
const configSchema = this.metadata.configSchema
|
|
if (configSchema) {
|
|
this.config.setSchema(this.name, {type: 'object', properties: configSchema})
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
registerConfigSchemaFromMainModule () {
|
|
if (this.mainModule && !this.configSchemaRegisteredOnLoad) {
|
|
if (typeof this.mainModule.config === 'object') {
|
|
this.config.setSchema(this.name, {type: 'object', properties: this.mainModule.config})
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TODO: Remove. Settings view calls this method currently.
|
|
activateConfig () {
|
|
if (this.configSchemaRegisteredOnLoad) return
|
|
this.requireMainModule()
|
|
this.registerConfigSchemaFromMainModule()
|
|
}
|
|
|
|
activateStylesheets () {
|
|
if (this.stylesheetsActivated) return
|
|
|
|
this.stylesheetDisposables = new CompositeDisposable()
|
|
|
|
const priority = this.getStyleSheetPriority()
|
|
for (let [sourcePath, source] of this.stylesheets) {
|
|
const match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./)
|
|
|
|
let context
|
|
if (match) {
|
|
context = match[1]
|
|
} else if (this.metadata.theme === 'syntax') {
|
|
context = 'atom-text-editor'
|
|
}
|
|
|
|
this.stylesheetDisposables.add(
|
|
this.styleManager.addStyleSheet(
|
|
source,
|
|
{
|
|
sourcePath,
|
|
priority,
|
|
context,
|
|
skipDeprecatedSelectorsTransformation: this.bundledPackage
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
this.stylesheetsActivated = true
|
|
}
|
|
|
|
activateResources () {
|
|
if (!this.activationDisposables) this.activationDisposables = new CompositeDisposable()
|
|
|
|
const packagesWithKeymapsDisabled = this.config.get('core.packagesWithKeymapsDisabled')
|
|
if (packagesWithKeymapsDisabled && packagesWithKeymapsDisabled.includes(this.name)) {
|
|
this.deactivateKeymaps()
|
|
} else if (!this.keymapActivated) {
|
|
this.activateKeymaps()
|
|
}
|
|
|
|
if (!this.menusActivated) {
|
|
this.activateMenus()
|
|
}
|
|
|
|
if (!this.grammarsActivated) {
|
|
for (let grammar of this.grammars) {
|
|
grammar.activate()
|
|
}
|
|
this.grammarsActivated = true
|
|
}
|
|
|
|
if (!this.settingsActivated) {
|
|
for (let settings of this.settings) {
|
|
settings.activate()
|
|
}
|
|
this.settingsActivated = true
|
|
}
|
|
}
|
|
|
|
activateKeymaps () {
|
|
if (this.keymapActivated) return
|
|
|
|
this.keymapDisposables = new CompositeDisposable()
|
|
|
|
const validateSelectors = !this.preloadedPackage
|
|
for (let [keymapPath, map] of this.keymaps) {
|
|
this.keymapDisposables.add(this.keymapManager.add(keymapPath, map, 0, validateSelectors))
|
|
}
|
|
this.menuManager.update()
|
|
|
|
this.keymapActivated = true
|
|
}
|
|
|
|
deactivateKeymaps () {
|
|
if (!this.keymapActivated) return
|
|
if (this.keymapDisposables) {
|
|
this.keymapDisposables.dispose()
|
|
}
|
|
this.menuManager.update()
|
|
this.keymapActivated = false
|
|
}
|
|
|
|
hasKeymaps () {
|
|
for (let [, map] of this.keymaps) {
|
|
if (map.length > 0) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
activateMenus () {
|
|
const validateSelectors = !this.preloadedPackage
|
|
for (const [menuPath, map] of this.menus) {
|
|
if (map['context-menu']) {
|
|
try {
|
|
const itemsBySelector = map['context-menu']
|
|
this.activationDisposables.add(this.contextMenuManager.add(itemsBySelector, validateSelectors))
|
|
} catch (error) {
|
|
if (error.code === 'EBADSELECTOR') {
|
|
error.message += ` in ${menuPath}`
|
|
error.stack += `\n at ${menuPath}:1:1`
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const [, map] of this.menus) {
|
|
if (map.menu) this.activationDisposables.add(this.menuManager.add(map.menu))
|
|
}
|
|
|
|
this.menusActivated = true
|
|
}
|
|
|
|
activateServices () {
|
|
let methodName, version, versions
|
|
for (var name in this.metadata.providedServices) {
|
|
({versions} = this.metadata.providedServices[name])
|
|
const servicesByVersion = {}
|
|
for (version in versions) {
|
|
methodName = versions[version]
|
|
if (typeof this.mainModule[methodName] === 'function') {
|
|
servicesByVersion[version] = this.mainModule[methodName]()
|
|
}
|
|
}
|
|
this.activationDisposables.add(this.packageManager.serviceHub.provide(name, servicesByVersion))
|
|
}
|
|
|
|
for (name in this.metadata.consumedServices) {
|
|
({versions} = this.metadata.consumedServices[name])
|
|
for (version in versions) {
|
|
methodName = versions[version]
|
|
if (typeof this.mainModule[methodName] === 'function') {
|
|
this.activationDisposables.add(this.packageManager.serviceHub.consume(name, version, this.mainModule[methodName].bind(this.mainModule)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
registerURIHandler () {
|
|
const handlerConfig = this.getURIHandler()
|
|
const methodName = handlerConfig && handlerConfig.method
|
|
if (methodName) {
|
|
this.uriHandlerSubscription = this.packageManager.registerURIHandlerForPackage(this.name, (...args) =>
|
|
this.handleURI(methodName, args)
|
|
)
|
|
}
|
|
}
|
|
|
|
unregisterURIHandler () {
|
|
if (this.uriHandlerSubscription) this.uriHandlerSubscription.dispose()
|
|
}
|
|
|
|
handleURI (methodName, args) {
|
|
this.activate().then(() => {
|
|
if (this.mainModule[methodName]) this.mainModule[methodName].apply(this.mainModule, args)
|
|
})
|
|
if (!this.mainActivated) this.activateNow()
|
|
}
|
|
|
|
registerTranspilerConfig () {
|
|
if (this.metadata.atomTranspilers) {
|
|
CompileCache.addTranspilerConfigForPath(this.path, this.name, this.metadata, this.metadata.atomTranspilers)
|
|
}
|
|
}
|
|
|
|
unregisterTranspilerConfig () {
|
|
if (this.metadata.atomTranspilers) {
|
|
CompileCache.removeTranspilerConfigForPath(this.path)
|
|
}
|
|
}
|
|
|
|
loadKeymaps () {
|
|
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
|
|
this.keymaps = []
|
|
for (const keymapPath in this.packageManager.packagesCache[this.name].keymaps) {
|
|
const keymapObject = this.packageManager.packagesCache[this.name].keymaps[keymapPath]
|
|
this.keymaps.push([`core:${keymapPath}`, keymapObject])
|
|
}
|
|
} else {
|
|
this.keymaps = this.getKeymapPaths().map((keymapPath) => [
|
|
keymapPath,
|
|
CSON.readFileSync(keymapPath, {allowDuplicateKeys: false}) || {}
|
|
])
|
|
}
|
|
}
|
|
|
|
loadMenus () {
|
|
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
|
|
this.menus = []
|
|
for (const menuPath in this.packageManager.packagesCache[this.name].menus) {
|
|
const menuObject = this.packageManager.packagesCache[this.name].menus[menuPath]
|
|
this.menus.push([`core:${menuPath}`, menuObject])
|
|
}
|
|
} else {
|
|
this.menus = this.getMenuPaths().map((menuPath) => [
|
|
menuPath,
|
|
CSON.readFileSync(menuPath) || {}
|
|
])
|
|
}
|
|
}
|
|
|
|
getKeymapPaths () {
|
|
const keymapsDirPath = path.join(this.path, 'keymaps')
|
|
if (this.metadata.keymaps) {
|
|
return this.metadata.keymaps.map(name => fs.resolve(keymapsDirPath, name, ['json', 'cson', '']))
|
|
} else {
|
|
return fs.listSync(keymapsDirPath, ['cson', 'json'])
|
|
}
|
|
}
|
|
|
|
getMenuPaths () {
|
|
const menusDirPath = path.join(this.path, 'menus')
|
|
if (this.metadata.menus) {
|
|
return this.metadata.menus.map(name => fs.resolve(menusDirPath, name, ['json', 'cson', '']))
|
|
} else {
|
|
return fs.listSync(menusDirPath, ['cson', 'json'])
|
|
}
|
|
}
|
|
|
|
loadStylesheets () {
|
|
this.stylesheets = this.getStylesheetPaths().map(stylesheetPath =>
|
|
[stylesheetPath, this.themeManager.loadStylesheet(stylesheetPath, true)]
|
|
)
|
|
}
|
|
|
|
registerDeserializerMethods () {
|
|
if (this.metadata.deserializers) {
|
|
Object.keys(this.metadata.deserializers).forEach(deserializerName => {
|
|
const methodName = this.metadata.deserializers[deserializerName]
|
|
this.deserializerManager.add({
|
|
name: deserializerName,
|
|
deserialize: (state, atomEnvironment) => {
|
|
this.registerViewProviders()
|
|
this.requireMainModule()
|
|
this.initializeIfNeeded()
|
|
return this.mainModule[methodName](state, atomEnvironment)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
activateCoreStartupServices () {
|
|
const directoryProviderService =
|
|
this.metadata.providedServices &&
|
|
this.metadata.providedServices['atom.directory-provider']
|
|
if (directoryProviderService) {
|
|
this.requireMainModule()
|
|
const servicesByVersion = {}
|
|
for (let version in directoryProviderService.versions) {
|
|
const methodName = directoryProviderService.versions[version]
|
|
if (typeof this.mainModule[methodName] === 'function') {
|
|
servicesByVersion[version] = this.mainModule[methodName]()
|
|
}
|
|
}
|
|
this.packageManager.serviceHub.provide('atom.directory-provider', servicesByVersion)
|
|
}
|
|
}
|
|
|
|
registerViewProviders () {
|
|
if (this.metadata.viewProviders && !this.registeredViewProviders) {
|
|
this.requireMainModule()
|
|
this.metadata.viewProviders.forEach(methodName => {
|
|
this.viewRegistry.addViewProvider(model => {
|
|
this.initializeIfNeeded()
|
|
return this.mainModule[methodName](model)
|
|
})
|
|
})
|
|
this.registeredViewProviders = true
|
|
}
|
|
}
|
|
|
|
getStylesheetsPath () {
|
|
return path.join(this.path, 'styles')
|
|
}
|
|
|
|
getStylesheetPaths () {
|
|
if (this.bundledPackage &&
|
|
this.packageManager.packagesCache[this.name] &&
|
|
this.packageManager.packagesCache[this.name].styleSheetPaths) {
|
|
const {styleSheetPaths} = this.packageManager.packagesCache[this.name]
|
|
return styleSheetPaths.map(styleSheetPath => path.join(this.path, styleSheetPath))
|
|
} else {
|
|
let indexStylesheet
|
|
const stylesheetDirPath = this.getStylesheetsPath()
|
|
if (this.metadata.mainStyleSheet) {
|
|
return [fs.resolve(this.path, this.metadata.mainStyleSheet)]
|
|
} else if (this.metadata.styleSheets) {
|
|
return this.metadata.styleSheets.map(name => fs.resolve(stylesheetDirPath, name, ['css', 'less', '']))
|
|
} else if ((indexStylesheet = fs.resolve(this.path, 'index', ['css', 'less']))) {
|
|
return [indexStylesheet]
|
|
} else {
|
|
return fs.listSync(stylesheetDirPath, ['css', 'less'])
|
|
}
|
|
}
|
|
}
|
|
|
|
loadGrammarsSync () {
|
|
if (this.grammarsLoaded) return
|
|
|
|
let grammarPaths
|
|
if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
|
|
({grammarPaths} = this.packageManager.packagesCache[this.name])
|
|
} else {
|
|
grammarPaths = fs.listSync(path.join(this.path, 'grammars'), ['json', 'cson'])
|
|
}
|
|
|
|
for (let grammarPath of grammarPaths) {
|
|
if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
|
|
grammarPath = path.resolve(this.packageManager.resourcePath, grammarPath)
|
|
}
|
|
|
|
try {
|
|
const grammar = this.grammarRegistry.readGrammarSync(grammarPath)
|
|
grammar.packageName = this.name
|
|
grammar.bundledPackage = this.bundledPackage
|
|
this.grammars.push(grammar)
|
|
grammar.activate()
|
|
} catch (error) {
|
|
console.warn(`Failed to load grammar: ${grammarPath}`, error.stack || error)
|
|
}
|
|
}
|
|
|
|
this.grammarsLoaded = true
|
|
this.grammarsActivated = true
|
|
}
|
|
|
|
loadGrammars () {
|
|
if (this.grammarsLoaded) return Promise.resolve()
|
|
|
|
const loadGrammar = (grammarPath, callback) => {
|
|
if (this.preloadedPackage) {
|
|
grammarPath = path.resolve(this.packageManager.resourcePath, grammarPath)
|
|
}
|
|
|
|
return this.grammarRegistry.readGrammar(grammarPath, (error, grammar) => {
|
|
if (error) {
|
|
const detail = `${error.message} in ${grammarPath}`
|
|
const stack = `${error.stack}\n at ${grammarPath}:1:1`
|
|
this.notificationManager.addFatalError(`Failed to load a ${this.name} package grammar`, {stack, detail, packageName: this.name, dismissable: true})
|
|
} else {
|
|
grammar.packageName = this.name
|
|
grammar.bundledPackage = this.bundledPackage
|
|
this.grammars.push(grammar)
|
|
if (this.grammarsActivated) grammar.activate()
|
|
}
|
|
return callback()
|
|
})
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
|
|
const { grammarPaths } = this.packageManager.packagesCache[this.name]
|
|
return async.each(grammarPaths, loadGrammar, () => resolve())
|
|
} else {
|
|
const grammarsDirPath = path.join(this.path, 'grammars')
|
|
fs.exists(grammarsDirPath, (grammarsDirExists) => {
|
|
if (!grammarsDirExists) return resolve()
|
|
fs.list(grammarsDirPath, ['json', 'cson'], (error, grammarPaths) => {
|
|
if (error || !grammarPaths) return resolve()
|
|
async.each(grammarPaths, loadGrammar, () => resolve())
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
loadSettings () {
|
|
this.settings = []
|
|
|
|
const loadSettingsFile = (settingsPath, callback) => {
|
|
return ScopedProperties.load(settingsPath, this.config, (error, settings) => {
|
|
if (error) {
|
|
const detail = `${error.message} in ${settingsPath}`
|
|
const stack = `${error.stack}\n at ${settingsPath}:1:1`
|
|
this.notificationManager.addFatalError(`Failed to load the ${this.name} package settings`, {stack, detail, packageName: this.name, dismissable: true})
|
|
} else {
|
|
this.settings.push(settings)
|
|
if (this.settingsActivated) { settings.activate() }
|
|
}
|
|
return callback()
|
|
})
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
|
|
for (let settingsPath in this.packageManager.packagesCache[this.name].settings) {
|
|
const scopedProperties = this.packageManager.packagesCache[this.name].settings[settingsPath]
|
|
const settings = new ScopedProperties(`core:${settingsPath}`, scopedProperties || {}, this.config)
|
|
this.settings.push(settings)
|
|
if (this.settingsActivated) { settings.activate() }
|
|
}
|
|
return resolve()
|
|
} else {
|
|
const settingsDirPath = path.join(this.path, 'settings')
|
|
fs.exists(settingsDirPath, (settingsDirExists) => {
|
|
if (!settingsDirExists) return resolve()
|
|
fs.list(settingsDirPath, ['json', 'cson'], (error, settingsPaths) => {
|
|
if (error || !settingsPaths) return resolve()
|
|
async.each(settingsPaths, loadSettingsFile, () => resolve())
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
serialize () {
|
|
if (this.mainActivated) {
|
|
if (typeof this.mainModule.serialize === 'function') {
|
|
try {
|
|
return this.mainModule.serialize()
|
|
} catch (error) {
|
|
console.error(`Error serializing package '${this.name}'`, error.stack)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async deactivate () {
|
|
this.activationPromise = null
|
|
this.resolveActivationPromise = null
|
|
if (this.activationCommandSubscriptions) this.activationCommandSubscriptions.dispose()
|
|
if (this.activationHookSubscriptions) this.activationHookSubscriptions.dispose()
|
|
this.configSchemaRegisteredOnActivate = false
|
|
this.unregisterURIHandler()
|
|
this.deactivateResources()
|
|
this.deactivateKeymaps()
|
|
|
|
if (!this.mainActivated) {
|
|
this.emitter.emit('did-deactivate')
|
|
return
|
|
}
|
|
|
|
if (typeof this.mainModule.deactivate === 'function') {
|
|
try {
|
|
const deactivationResult = this.mainModule.deactivate()
|
|
if (deactivationResult && typeof deactivationResult.then === 'function') {
|
|
await deactivationResult
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error deactivating package '${this.name}'`, error.stack)
|
|
}
|
|
}
|
|
|
|
if (typeof this.mainModule.deactivateConfig === 'function') {
|
|
try {
|
|
await this.mainModule.deactivateConfig()
|
|
} catch (error) {
|
|
console.error(`Error deactivating package '${this.name}'`, error.stack)
|
|
}
|
|
}
|
|
|
|
this.mainActivated = false
|
|
this.mainInitialized = false
|
|
this.emitter.emit('did-deactivate')
|
|
}
|
|
|
|
deactivateResources () {
|
|
for (let grammar of this.grammars) {
|
|
grammar.deactivate()
|
|
}
|
|
for (let settings of this.settings) {
|
|
settings.deactivate()
|
|
}
|
|
|
|
if (this.stylesheetDisposables) this.stylesheetDisposables.dispose()
|
|
if (this.activationDisposables) this.activationDisposables.dispose()
|
|
if (this.keymapDisposables) this.keymapDisposables.dispose()
|
|
|
|
this.stylesheetsActivated = false
|
|
this.grammarsActivated = false
|
|
this.settingsActivated = false
|
|
this.menusActivated = false
|
|
}
|
|
|
|
reloadStylesheets () {
|
|
try {
|
|
this.loadStylesheets()
|
|
} catch (error) {
|
|
this.handleError(`Failed to reload the ${this.name} package stylesheets`, error)
|
|
}
|
|
|
|
if (this.stylesheetDisposables) this.stylesheetDisposables.dispose()
|
|
this.stylesheetDisposables = new CompositeDisposable()
|
|
this.stylesheetsActivated = false
|
|
this.activateStylesheets()
|
|
}
|
|
|
|
requireMainModule () {
|
|
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
|
|
if (this.packageManager.packagesCache[this.name].main) {
|
|
this.mainModule = require(this.packageManager.packagesCache[this.name].main)
|
|
return this.mainModule
|
|
}
|
|
} else if (this.mainModuleRequired) {
|
|
return this.mainModule
|
|
} else if (!this.isCompatible()) {
|
|
const nativeModuleNames = this.incompatibleModules.map(m => m.name).join(', ')
|
|
console.warn(dedent `
|
|
Failed to require the main module of '${this.name}' because it requires one or more incompatible native modules (${nativeModuleNames}).
|
|
Run \`apm rebuild\` in the package directory and restart Atom to resolve.\
|
|
`)
|
|
} else {
|
|
const mainModulePath = this.getMainModulePath()
|
|
if (fs.isFileSync(mainModulePath)) {
|
|
this.mainModuleRequired = true
|
|
|
|
const previousViewProviderCount = this.viewRegistry.getViewProviderCount()
|
|
const previousDeserializerCount = this.deserializerManager.getDeserializerCount()
|
|
this.mainModule = require(mainModulePath)
|
|
if ((this.viewRegistry.getViewProviderCount() === previousViewProviderCount) &&
|
|
(this.deserializerManager.getDeserializerCount() === previousDeserializerCount)) {
|
|
localStorage.setItem(this.getCanDeferMainModuleRequireStorageKey(), 'true')
|
|
}
|
|
return this.mainModule
|
|
}
|
|
}
|
|
}
|
|
|
|
getMainModulePath () {
|
|
if (this.resolvedMainModulePath) return this.mainModulePath
|
|
this.resolvedMainModulePath = true
|
|
|
|
if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
|
|
if (this.packageManager.packagesCache[this.name].main) {
|
|
this.mainModulePath = path.resolve(this.packageManager.resourcePath, 'static', this.packageManager.packagesCache[this.name].main)
|
|
} else {
|
|
this.mainModulePath = null
|
|
}
|
|
} else {
|
|
const mainModulePath = this.metadata.main
|
|
? path.join(this.path, this.metadata.main)
|
|
: path.join(this.path, 'index')
|
|
this.mainModulePath = fs.resolveExtension(mainModulePath, ['', ...CompileCache.supportedExtensions])
|
|
}
|
|
return this.mainModulePath
|
|
}
|
|
|
|
activationShouldBeDeferred () {
|
|
return this.hasActivationCommands() || this.hasActivationHooks() || this.hasDeferredURIHandler()
|
|
}
|
|
|
|
hasActivationHooks () {
|
|
const hooks = this.getActivationHooks()
|
|
return hooks && hooks.length > 0
|
|
}
|
|
|
|
hasActivationCommands () {
|
|
const object = this.getActivationCommands()
|
|
for (let selector in object) {
|
|
const commands = object[selector]
|
|
if (commands.length > 0) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
hasDeferredURIHandler () {
|
|
const handler = this.getURIHandler()
|
|
return handler && handler.deferActivation !== false
|
|
}
|
|
|
|
subscribeToDeferredActivation () {
|
|
this.subscribeToActivationCommands()
|
|
this.subscribeToActivationHooks()
|
|
}
|
|
|
|
subscribeToActivationCommands () {
|
|
this.activationCommandSubscriptions = new CompositeDisposable()
|
|
const object = this.getActivationCommands()
|
|
for (let selector in object) {
|
|
const commands = object[selector]
|
|
for (let command of commands) {
|
|
((selector, command) => {
|
|
// Add dummy command so it appears in menu.
|
|
// The real command will be registered on package activation
|
|
try {
|
|
this.activationCommandSubscriptions.add(this.commandRegistry.add(selector, command, function () {}))
|
|
} catch (error) {
|
|
if (error.code === 'EBADSELECTOR') {
|
|
const metadataPath = path.join(this.path, 'package.json')
|
|
error.message += ` in ${metadataPath}`
|
|
error.stack += `\n at ${metadataPath}:1:1`
|
|
}
|
|
throw error
|
|
}
|
|
|
|
this.activationCommandSubscriptions.add(this.commandRegistry.onWillDispatch(event => {
|
|
if (event.type !== command) return
|
|
let currentTarget = event.target
|
|
while (currentTarget) {
|
|
if (currentTarget.webkitMatchesSelector(selector)) {
|
|
this.activationCommandSubscriptions.dispose()
|
|
this.activateNow()
|
|
break
|
|
}
|
|
currentTarget = currentTarget.parentElement
|
|
}
|
|
}))
|
|
})(selector, command)
|
|
}
|
|
}
|
|
}
|
|
|
|
getActivationCommands () {
|
|
if (this.activationCommands) return this.activationCommands
|
|
|
|
this.activationCommands = {}
|
|
|
|
if (this.metadata.activationCommands) {
|
|
for (let selector in this.metadata.activationCommands) {
|
|
const commands = this.metadata.activationCommands[selector]
|
|
if (!this.activationCommands[selector]) this.activationCommands[selector] = []
|
|
if (typeof commands === 'string') {
|
|
this.activationCommands[selector].push(commands)
|
|
} else if (Array.isArray(commands)) {
|
|
this.activationCommands[selector].push(...commands)
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.activationCommands
|
|
}
|
|
|
|
subscribeToActivationHooks () {
|
|
this.activationHookSubscriptions = new CompositeDisposable()
|
|
for (let hook of this.getActivationHooks()) {
|
|
if (typeof hook === 'string' && hook.trim().length > 0) {
|
|
this.activationHookSubscriptions.add(
|
|
this.packageManager.onDidTriggerActivationHook(hook, () => this.activateNow())
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
getActivationHooks () {
|
|
if (this.metadata && this.activationHooks) return this.activationHooks
|
|
|
|
if (this.metadata.activationHooks) {
|
|
if (Array.isArray(this.metadata.activationHooks)) {
|
|
this.activationHooks = Array.from(new Set(this.metadata.activationHooks))
|
|
} else if (typeof this.metadata.activationHooks === 'string') {
|
|
this.activationHooks = [this.metadata.activationHooks]
|
|
} else {
|
|
this.activationHooks = []
|
|
}
|
|
} else {
|
|
this.activationHooks = []
|
|
}
|
|
|
|
return this.activationHooks
|
|
}
|
|
|
|
getURIHandler () {
|
|
return this.metadata && this.metadata.uriHandler
|
|
}
|
|
|
|
// Does the given module path contain native code?
|
|
isNativeModule (modulePath) {
|
|
try {
|
|
return fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node']).length > 0
|
|
} catch (error) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Get an array of all the native modules that this package depends on.
|
|
//
|
|
// First try to get this information from
|
|
// @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't
|
|
// exist, recurse through all dependencies.
|
|
getNativeModuleDependencyPaths () {
|
|
const nativeModulePaths = []
|
|
|
|
if (this.metadata._atomModuleCache) {
|
|
const relativeNativeModuleBindingPaths =
|
|
(this.metadata._atomModuleCache.extensions && this.metadata._atomModuleCache.extensions['.node']) ||
|
|
[]
|
|
for (let relativeNativeModuleBindingPath of relativeNativeModuleBindingPaths) {
|
|
const nativeModulePath = path.join(this.path, relativeNativeModuleBindingPath, '..', '..', '..')
|
|
nativeModulePaths.push(nativeModulePath)
|
|
}
|
|
return nativeModulePaths
|
|
}
|
|
|
|
var traversePath = nodeModulesPath => {
|
|
try {
|
|
for (let modulePath of fs.listSync(nodeModulesPath)) {
|
|
if (this.isNativeModule(modulePath)) nativeModulePaths.push(modulePath)
|
|
traversePath(path.join(modulePath, 'node_modules'))
|
|
}
|
|
} catch (error) {}
|
|
}
|
|
|
|
traversePath(path.join(this.path, 'node_modules'))
|
|
|
|
return nativeModulePaths
|
|
}
|
|
|
|
/*
|
|
Section: Native Module Compatibility
|
|
*/
|
|
|
|
// Extended: Are all native modules depended on by this package correctly
|
|
// compiled against the current version of Atom?
|
|
//
|
|
// Incompatible packages cannot be activated.
|
|
//
|
|
// Returns a {Boolean}, true if compatible, false if incompatible.
|
|
isCompatible () {
|
|
if (this.compatible == null) {
|
|
if (this.preloadedPackage) {
|
|
this.compatible = true
|
|
} else if (this.getMainModulePath()) {
|
|
this.incompatibleModules = this.getIncompatibleNativeModules()
|
|
this.compatible =
|
|
this.incompatibleModules.length === 0 &&
|
|
this.getBuildFailureOutput() == null
|
|
} else {
|
|
this.compatible = true
|
|
}
|
|
}
|
|
return this.compatible
|
|
}
|
|
|
|
// Extended: Rebuild native modules in this package's dependencies for the
|
|
// current version of Atom.
|
|
//
|
|
// Returns a {Promise} that resolves with an object containing `code`,
|
|
// `stdout`, and `stderr` properties based on the results of running
|
|
// `apm rebuild` on the package.
|
|
rebuild () {
|
|
return new Promise(resolve =>
|
|
this.runRebuildProcess(result => {
|
|
if (result.code === 0) {
|
|
global.localStorage.removeItem(this.getBuildFailureOutputStorageKey())
|
|
} else {
|
|
this.compatible = false
|
|
global.localStorage.setItem(this.getBuildFailureOutputStorageKey(), result.stderr)
|
|
}
|
|
global.localStorage.setItem(this.getIncompatibleNativeModulesStorageKey(), '[]')
|
|
resolve(result)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Extended: If a previous rebuild failed, get the contents of stderr.
|
|
//
|
|
// Returns a {String} or null if no previous build failure occurred.
|
|
getBuildFailureOutput () {
|
|
return global.localStorage.getItem(this.getBuildFailureOutputStorageKey())
|
|
}
|
|
|
|
runRebuildProcess (done) {
|
|
let stderr = ''
|
|
let stdout = ''
|
|
return new BufferedProcess({
|
|
command: this.packageManager.getApmPath(),
|
|
args: ['rebuild', '--no-color'],
|
|
options: {cwd: this.path},
|
|
stderr (output) { stderr += output },
|
|
stdout (output) { stdout += output },
|
|
exit (code) { done({code, stdout, stderr}) }
|
|
})
|
|
}
|
|
|
|
getBuildFailureOutputStorageKey () {
|
|
return `installed-packages:${this.name}:${this.metadata.version}:build-error`
|
|
}
|
|
|
|
getIncompatibleNativeModulesStorageKey () {
|
|
const electronVersion = process.versions.electron
|
|
return `installed-packages:${this.name}:${this.metadata.version}:electron-${electronVersion}:incompatible-native-modules`
|
|
}
|
|
|
|
getCanDeferMainModuleRequireStorageKey () {
|
|
return `installed-packages:${this.name}:${this.metadata.version}:can-defer-main-module-require`
|
|
}
|
|
|
|
// 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 () {
|
|
if (!this.packageManager.devMode) {
|
|
try {
|
|
const arrayAsString = global.localStorage.getItem(this.getIncompatibleNativeModulesStorageKey())
|
|
if (arrayAsString) return JSON.parse(arrayAsString)
|
|
} catch (error1) {}
|
|
}
|
|
|
|
const incompatibleNativeModules = []
|
|
for (let nativeModulePath of this.getNativeModuleDependencyPaths()) {
|
|
try {
|
|
require(nativeModulePath)
|
|
} catch (error) {
|
|
let version
|
|
try {
|
|
({version} = require(`${nativeModulePath}/package.json`))
|
|
} catch (error2) {}
|
|
incompatibleNativeModules.push({
|
|
path: nativeModulePath,
|
|
name: path.basename(nativeModulePath),
|
|
version,
|
|
error: error.message
|
|
})
|
|
}
|
|
}
|
|
|
|
global.localStorage.setItem(
|
|
this.getIncompatibleNativeModulesStorageKey(),
|
|
JSON.stringify(incompatibleNativeModules)
|
|
)
|
|
|
|
return incompatibleNativeModules
|
|
}
|
|
|
|
handleError (message, error) {
|
|
if (atom.inSpecMode()) throw error
|
|
|
|
let detail, location, stack
|
|
if (error.filename && error.location && error instanceof SyntaxError) {
|
|
location = `${error.filename}:${error.location.first_line + 1}:${error.location.first_column + 1}`
|
|
detail = `${error.message} in ${location}`
|
|
stack = 'SyntaxError: ' + error.message + '\n' + 'at ' + location
|
|
} else if (error.less && error.filename && error.column != null && error.line != null) {
|
|
location = `${error.filename}:${error.line}:${error.column}`
|
|
detail = `${error.message} in ${location}`
|
|
stack = 'LessError: ' + error.message + '\n' + 'at ' + location
|
|
} else {
|
|
detail = error.message
|
|
stack = error.stack || error
|
|
}
|
|
|
|
this.notificationManager.addFatalError(message, {
|
|
stack, detail, packageName: this.name, dismissable: true
|
|
})
|
|
}
|
|
}
|