Files
atom/src/package.js
Landon Abney 53c0f5f4a0 🐛 Prefer package name from metadata
When loading a package prefer the name specified in the package metadata
instead of the name of the folder that the package's files reside in.

Fixes #16703.
2018-02-14 12:02:17 -08:00

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 =
(this.metadata && this.metadata.name) ||
params.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
})
}
}