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 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.config) } 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.config) } 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 SettingsFile.load(settingsPath, (error, settingsFile) => { 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(settingsFile) if (this.settingsActivated) settingsFile.activate(this.config) } 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 properties = this.packageManager.packagesCache[this.name].settings[settingsPath] const settingsFile = new SettingsFile(`core:${settingsPath}`, properties || {}) this.settings.push(settingsFile) if (this.settingsActivated) settingsFile.activate(this.config) } 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(this.config) } 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 }) } } class SettingsFile { static load (path, callback) { CSON.readFile(path, (error, properties = {}) => { if (error) { callback(error) } else { callback(null, new SettingsFile(path, properties)) } }) } constructor (path, properties) { this.path = path this.properties = properties } activate (config) { for (let selector in this.properties) { config.set(null, this.properties[selector], {scopeSelector: selector, source: this.path}) } } deactivate (config) { for (let selector in this.properties) { config.unset(null, {scopeSelector: selector, source: this.path}) } } }