diff --git a/src/package.coffee b/src/package.coffee deleted file mode 100644 index 1635c75dc..000000000 --- a/src/package.coffee +++ /dev/null @@ -1,848 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -async = require 'async' -CSON = require 'season' -fs = require 'fs-plus' -{Emitter, CompositeDisposable} = require 'event-kit' - -CompileCache = require './compile-cache' -ModuleCache = require './module-cache' -ScopedProperties = require './scoped-properties' -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 - keymaps: null - menus: null - stylesheets: null - stylesheetDisposables: null - grammars: null - settings: null - mainModulePath: null - resolvedMainModulePath: false - mainModule: null - mainInitialized: false - mainActivated: false - - ### - Section: Construction - ### - - constructor: (params) -> - { - @path, @metadata, @bundledPackage, @preloadedPackage, @packageManager, @config, @styleManager, @commandRegistry, - @keymapManager, @notificationManager, @grammarRegistry, @themeManager, - @menuManager, @contextMenuManager, @deserializerManager, @viewRegistry - } = params - - @emitter = new Emitter - @metadata ?= @packageManager.loadPackageMetadata(@path) - @bundledPackage ?= @packageManager.isBundledPackagePath(@path) - @name = @metadata?.name ? params.name ? path.basename(@path) - @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) -> - @emitter.on 'did-deactivate', callback - - ### - Section: Instance Methods - ### - - enable: -> - @config.removeAtKeyPath('core.disabledPackages', @name) - - disable: -> - @config.pushAtKeyPath('core.disabledPackages', @name) - - isTheme: -> - @metadata?.theme? - - measure: (key, fn) -> - startTime = Date.now() - value = fn() - @[key] = Date.now() - startTime - value - - getType: -> 'atom' - - getStyleSheetPriority: -> 0 - - preload: -> - @loadKeymaps() - @loadMenus() - @registerDeserializerMethods() - @activateCoreStartupServices() - @registerURIHandler() - @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() - @requireMainModule() - @settingsPromise = @loadSettings() - - @activationDisposables = new CompositeDisposable - @activateKeymaps() - @activateMenus() - settings.activate() for settings in @settings - @settingsActivated = true - - finishLoading: -> - @measure 'loadTime', => - @path = path.join(@packageManager.resourcePath, @path) - ModuleCache.add(@path, @metadata) - - @loadStylesheets() - # Unfortunately some packages are accessing `@mainModulePath`, so we need - # to compute that variable eagerly also for preloaded packages. - @getMainModulePath() - - load: -> - @measure 'loadTime', => - try - ModuleCache.add(@path, @metadata) - - @loadKeymaps() - @loadMenus() - @loadStylesheets() - @registerDeserializerMethods() - @activateCoreStartupServices() - @registerURIHandler() - @registerTranspilerConfig() - @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() - @settingsPromise = @loadSettings() - if @shouldRequireMainModuleOnLoad() and not @mainModule? - @requireMainModule() - catch error - @handleError("Failed to load the #{@name} package", error) - this - - unload: -> - @unregisterTranspilerConfig() - - shouldRequireMainModuleOnLoad: -> - not ( - @metadata.deserializers? or - @metadata.viewProviders? or - @metadata.configSchema? or - @activationShouldBeDeferred() or - localStorage.getItem(@getCanDeferMainModuleRequireStorageKey()) is 'true' - ) - - reset: -> - @stylesheets = [] - @keymaps = [] - @menus = [] - @grammars = [] - @settings = [] - @mainInitialized = false - @mainActivated = false - - initializeIfNeeded: -> - return if @mainInitialized - @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. - @requireMainModule() unless @mainModule? - @mainModule.initialize?(@packageManager.getPackageState(@name) ? {}) - @mainInitialized = true - catch error - @handleError("Failed to initialize the #{@name} package", error) - return - - activate: -> - @grammarsPromise ?= @loadGrammars() - @activationPromise ?= - new Promise (resolve, reject) => - @resolveActivationPromise = resolve - @measure 'activateTime', => - try - @activateResources() - if @activationShouldBeDeferred() - @subscribeToDeferredActivation() - else - @activateNow() - catch error - @handleError("Failed to activate the #{@name} package", error) - - Promise.all([@grammarsPromise, @settingsPromise, @activationPromise]) - - activateNow: -> - try - @requireMainModule() unless @mainModule? - @configSchemaRegisteredOnActivate = @registerConfigSchemaFromMainModule() - @registerViewProviders() - @activateStylesheets() - if @mainModule? and not @mainActivated - @initializeIfNeeded() - @mainModule.activateConfig?() - @mainModule.activate?(@packageManager.getPackageState(@name) ? {}) - @mainActivated = true - @activateServices() - @activationCommandSubscriptions?.dispose() - @activationHookSubscriptions?.dispose() - catch error - @handleError("Failed to activate the #{@name} package", error) - - @resolveActivationPromise?() - - registerConfigSchemaFromMetadata: -> - if configSchema = @metadata.configSchema - @config.setSchema @name, {type: 'object', properties: configSchema} - true - else - false - - registerConfigSchemaFromMainModule: -> - if @mainModule? and not @configSchemaRegisteredOnLoad - if @mainModule.config? and typeof @mainModule.config is 'object' - @config.setSchema @name, {type: 'object', properties: @mainModule.config} - return true - false - - # TODO: Remove. Settings view calls this method currently. - activateConfig: -> - return if @configSchemaRegisteredOnLoad - @requireMainModule() - @registerConfigSchemaFromMainModule() - - activateStylesheets: -> - return if @stylesheetsActivated - - @stylesheetDisposables = new CompositeDisposable - - priority = @getStyleSheetPriority() - for [sourcePath, source] in @stylesheets - if match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./) - context = match[1] - else if @metadata.theme is 'syntax' - context = 'atom-text-editor' - else - context = undefined - - @stylesheetDisposables.add( - @styleManager.addStyleSheet( - source, - { - sourcePath, - priority, - context, - skipDeprecatedSelectorsTransformation: @bundledPackage - } - ) - ) - @stylesheetsActivated = true - - activateResources: -> - @activationDisposables ?= new CompositeDisposable - - keymapIsDisabled = _.include(@config.get("core.packagesWithKeymapsDisabled") ? [], @name) - if keymapIsDisabled - @deactivateKeymaps() - else unless @keymapActivated - @activateKeymaps() - - unless @menusActivated - @activateMenus() - - unless @grammarsActivated - grammar.activate() for grammar in @grammars - @grammarsActivated = true - - unless @settingsActivated - settings.activate() for settings in @settings - @settingsActivated = true - - activateKeymaps: -> - return if @keymapActivated - - @keymapDisposables = new CompositeDisposable() - - validateSelectors = not @preloadedPackage - @keymapDisposables.add(@keymapManager.add(keymapPath, map, 0, validateSelectors)) for [keymapPath, map] in @keymaps - @menuManager.update() - - @keymapActivated = true - - deactivateKeymaps: -> - return if not @keymapActivated - - @keymapDisposables?.dispose() - @menuManager.update() - - @keymapActivated = false - - hasKeymaps: -> - for [path, map] in @keymaps - if map.length > 0 - return true - false - - activateMenus: -> - validateSelectors = not @preloadedPackage - for [menuPath, map] in @menus when map['context-menu']? - try - itemsBySelector = map['context-menu'] - @activationDisposables.add(@contextMenuManager.add(itemsBySelector, validateSelectors)) - catch error - if error.code is 'EBADSELECTOR' - error.message += " in #{menuPath}" - error.stack += "\n at #{menuPath}:1:1" - throw error - - for [menuPath, map] in @menus when map['menu']? - @activationDisposables.add(@menuManager.add(map['menu'])) - - @menusActivated = true - - activateServices: -> - for name, {versions} of @metadata.providedServices - servicesByVersion = {} - for version, methodName of versions - if typeof @mainModule[methodName] is 'function' - servicesByVersion[version] = @mainModule[methodName]() - @activationDisposables.add @packageManager.serviceHub.provide(name, servicesByVersion) - - for name, {versions} of @metadata.consumedServices - for version, methodName of versions - if typeof @mainModule[methodName] is 'function' - @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) - return - - registerURIHandler: -> - handlerConfig = @getURIHandler() - if methodName = handlerConfig?.method - @uriHandlerSubscription = @packageManager.registerURIHandlerForPackage @name, (args...) => - @handleURI(methodName, args) - - unregisterURIHandler: -> - @uriHandlerSubscription?.dispose() - - handleURI: (methodName, args) -> - @activate().then => @mainModule[methodName]?.apply(@mainModule, args) - @activateNow() unless @mainActivated - - registerTranspilerConfig: -> - if @metadata.atomTranspilers - CompileCache.addTranspilerConfigForPath(@path, @name, @metadata, @metadata.atomTranspilers) - - unregisterTranspilerConfig: -> - if @metadata.atomTranspilers - CompileCache.removeTranspilerConfigForPath(@path) - - loadKeymaps: -> - if @bundledPackage and @packageManager.packagesCache[@name]? - @keymaps = (["core:#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps) - else - @keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath, allowDuplicateKeys: false) ? {}] - return - - loadMenus: -> - if @bundledPackage and @packageManager.packagesCache[@name]? - @menus = (["core:#{menuPath}", menuObject] for menuPath, menuObject of @packageManager.packagesCache[@name].menus) - else - @menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}] - return - - getKeymapPaths: -> - keymapsDirPath = path.join(@path, 'keymaps') - if @metadata.keymaps - @metadata.keymaps.map (name) -> fs.resolve(keymapsDirPath, name, ['json', 'cson', '']) - else - fs.listSync(keymapsDirPath, ['cson', 'json']) - - getMenuPaths: -> - menusDirPath = path.join(@path, 'menus') - if @metadata.menus - @metadata.menus.map (name) -> fs.resolve(menusDirPath, name, ['json', 'cson', '']) - else - fs.listSync(menusDirPath, ['cson', 'json']) - - loadStylesheets: -> - @stylesheets = @getStylesheetPaths().map (stylesheetPath) => - [stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)] - - registerDeserializerMethods: -> - if @metadata.deserializers? - Object.keys(@metadata.deserializers).forEach (deserializerName) => - methodName = @metadata.deserializers[deserializerName] - @deserializerManager.add - name: deserializerName, - deserialize: (state, atomEnvironment) => - @registerViewProviders() - @requireMainModule() - @initializeIfNeeded() - @mainModule[methodName](state, atomEnvironment) - return - - activateCoreStartupServices: -> - if directoryProviderService = @metadata.providedServices?['atom.directory-provider'] - @requireMainModule() - servicesByVersion = {} - for version, methodName of directoryProviderService.versions - if typeof @mainModule[methodName] is 'function' - servicesByVersion[version] = @mainModule[methodName]() - @packageManager.serviceHub.provide('atom.directory-provider', servicesByVersion) - - registerViewProviders: -> - if @metadata.viewProviders? and not @registeredViewProviders - @requireMainModule() - @metadata.viewProviders.forEach (methodName) => - @viewRegistry.addViewProvider (model) => - @initializeIfNeeded() - @mainModule[methodName](model) - @registeredViewProviders = true - - getStylesheetsPath: -> - path.join(@path, 'styles') - - getStylesheetPaths: -> - if @bundledPackage and @packageManager.packagesCache[@name]?.styleSheetPaths? - styleSheetPaths = @packageManager.packagesCache[@name].styleSheetPaths - styleSheetPaths.map (styleSheetPath) => path.join(@path, styleSheetPath) - else - stylesheetDirPath = @getStylesheetsPath() - if @metadata.mainStyleSheet - [fs.resolve(@path, @metadata.mainStyleSheet)] - else if @metadata.styleSheets - @metadata.styleSheets.map (name) -> fs.resolve(stylesheetDirPath, name, ['css', 'less', '']) - else if indexStylesheet = fs.resolve(@path, 'index', ['css', 'less']) - [indexStylesheet] - else - fs.listSync(stylesheetDirPath, ['css', 'less']) - - loadGrammarsSync: -> - return if @grammarsLoaded - - if @preloadedPackage and @packageManager.packagesCache[@name]? - grammarPaths = @packageManager.packagesCache[@name].grammarPaths - else - grammarPaths = fs.listSync(path.join(@path, 'grammars'), ['json', 'cson']) - - for grammarPath in grammarPaths - if @preloadedPackage and @packageManager.packagesCache[@name]? - grammarPath = path.resolve(@packageManager.resourcePath, grammarPath) - - try - grammar = @grammarRegistry.readGrammarSync(grammarPath) - grammar.packageName = @name - grammar.bundledPackage = @bundledPackage - @grammars.push(grammar) - grammar.activate() - catch error - console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error) - - @grammarsLoaded = true - @grammarsActivated = true - - loadGrammars: -> - return Promise.resolve() if @grammarsLoaded - - loadGrammar = (grammarPath, callback) => - if @preloadedPackage - grammarPath = path.resolve(@packageManager.resourcePath, grammarPath) - - @grammarRegistry.readGrammar grammarPath, (error, grammar) => - if error? - detail = "#{error.message} in #{grammarPath}" - stack = "#{error.stack}\n at #{grammarPath}:1:1" - @notificationManager.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, packageName: @name, dismissable: true}) - else - grammar.packageName = @name - grammar.bundledPackage = @bundledPackage - @grammars.push(grammar) - grammar.activate() if @grammarsActivated - callback() - - new Promise (resolve) => - if @preloadedPackage and @packageManager.packagesCache[@name]? - grammarPaths = @packageManager.packagesCache[@name].grammarPaths - async.each grammarPaths, loadGrammar, -> resolve() - else - grammarsDirPath = path.join(@path, 'grammars') - fs.exists grammarsDirPath, (grammarsDirExists) -> - return resolve() unless grammarsDirExists - - fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) -> - async.each grammarPaths, loadGrammar, -> resolve() - - loadSettings: -> - @settings = [] - - loadSettingsFile = (settingsPath, callback) => - ScopedProperties.load settingsPath, @config, (error, settings) => - if error? - detail = "#{error.message} in #{settingsPath}" - stack = "#{error.stack}\n at #{settingsPath}:1:1" - @notificationManager.addFatalError("Failed to load the #{@name} package settings", {stack, detail, packageName: @name, dismissable: true}) - else - @settings.push(settings) - settings.activate() if @settingsActivated - callback() - - new Promise (resolve) => - if @preloadedPackage and @packageManager.packagesCache[@name]? - for settingsPath, scopedProperties of @packageManager.packagesCache[@name].settings - settings = new ScopedProperties("core:#{settingsPath}", scopedProperties ? {}, @config) - @settings.push(settings) - settings.activate() if @settingsActivated - resolve() - else - settingsDirPath = path.join(@path, 'settings') - fs.exists settingsDirPath, (settingsDirExists) -> - return resolve() unless settingsDirExists - - fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) -> - async.each settingsPaths, loadSettingsFile, -> resolve() - - serialize: -> - if @mainActivated - try - @mainModule?.serialize?() - catch e - console.error "Error serializing package '#{@name}'", e.stack - - deactivate: -> - @activationPromise = null - @resolveActivationPromise = null - @activationCommandSubscriptions?.dispose() - @activationHookSubscriptions?.dispose() - @configSchemaRegisteredOnActivate = false - @unregisterURIHandler() - @deactivateResources() - @deactivateKeymaps() - - unless @mainActivated - @emitter.emit 'did-deactivate' - return - - try - deactivationResult = @mainModule?.deactivate?() - catch e - console.error "Error deactivating package '#{@name}'", e.stack - - # We support then-able async promises as well as sync ones from deactivate - if typeof deactivationResult?.then is 'function' - deactivationResult.then => @afterDeactivation() - else - @afterDeactivation() - - afterDeactivation: -> - try - @mainModule?.deactivateConfig?() - catch e - console.error "Error deactivating package '#{@name}'", e.stack - @mainActivated = false - @mainInitialized = false - @emitter.emit 'did-deactivate' - - deactivateResources: -> - grammar.deactivate() for grammar in @grammars - settings.deactivate() for settings in @settings - @stylesheetDisposables?.dispose() - @activationDisposables?.dispose() - @keymapDisposables?.dispose() - @stylesheetsActivated = false - @grammarsActivated = false - @settingsActivated = false - @menusActivated = false - - reloadStylesheets: -> - try - @loadStylesheets() - catch error - @handleError("Failed to reload the #{@name} package stylesheets", error) - - @stylesheetDisposables?.dispose() - @stylesheetDisposables = new CompositeDisposable - @stylesheetsActivated = false - @activateStylesheets() - - requireMainModule: -> - if @bundledPackage and @packageManager.packagesCache[@name]? - if @packageManager.packagesCache[@name].main? - @mainModule = require(@packageManager.packagesCache[@name].main) - else if @mainModuleRequired - @mainModule - else if not @isCompatible() - console.warn """ - Failed to require the main module of '#{@name}' because it requires one or more incompatible native modules (#{_.pluck(@incompatibleModules, 'name').join(', ')}). - Run `apm rebuild` in the package directory and restart Atom to resolve. - """ - return - else - mainModulePath = @getMainModulePath() - if fs.isFileSync(mainModulePath) - @mainModuleRequired = true - - previousViewProviderCount = @viewRegistry.getViewProviderCount() - previousDeserializerCount = @deserializerManager.getDeserializerCount() - @mainModule = require(mainModulePath) - if (@viewRegistry.getViewProviderCount() is previousViewProviderCount and - @deserializerManager.getDeserializerCount() is previousDeserializerCount) - localStorage.setItem(@getCanDeferMainModuleRequireStorageKey(), 'true') - - getMainModulePath: -> - return @mainModulePath if @resolvedMainModulePath - @resolvedMainModulePath = true - - if @bundledPackage and @packageManager.packagesCache[@name]? - if @packageManager.packagesCache[@name].main - @mainModulePath = path.resolve(@packageManager.resourcePath, 'static', @packageManager.packagesCache[@name].main) - else - @mainModulePath = null - else - mainModulePath = - if @metadata.main - path.join(@path, @metadata.main) - else - path.join(@path, 'index') - @mainModulePath = fs.resolveExtension(mainModulePath, ["", CompileCache.supportedExtensions...]) - - activationShouldBeDeferred: -> - @hasActivationCommands() or @hasActivationHooks() or @hasDeferredURIHandler() - - hasActivationHooks: -> - @getActivationHooks()?.length > 0 - - hasActivationCommands: -> - for selector, commands of @getActivationCommands() - return true if commands.length > 0 - false - - hasDeferredURIHandler: -> - @getURIHandler() and @getURIHandler().deferActivation isnt false - - subscribeToDeferredActivation: -> - @subscribeToActivationCommands() - @subscribeToActivationHooks() - - subscribeToActivationCommands: -> - @activationCommandSubscriptions = new CompositeDisposable - for selector, commands of @getActivationCommands() - for command in commands - do (selector, command) => - # Add dummy command so it appears in menu. - # The real command will be registered on package activation - try - @activationCommandSubscriptions.add @commandRegistry.add selector, command, -> - catch error - if error.code is 'EBADSELECTOR' - metadataPath = path.join(@path, 'package.json') - error.message += " in #{metadataPath}" - error.stack += "\n at #{metadataPath}:1:1" - throw error - - @activationCommandSubscriptions.add @commandRegistry.onWillDispatch (event) => - return unless event.type is command - currentTarget = event.target - while currentTarget - if currentTarget.webkitMatchesSelector(selector) - @activationCommandSubscriptions.dispose() - @activateNow() - break - currentTarget = currentTarget.parentElement - return - return - - getActivationCommands: -> - return @activationCommands if @activationCommands? - - @activationCommands = {} - - if @metadata.activationCommands? - for selector, commands of @metadata.activationCommands - @activationCommands[selector] ?= [] - if _.isString(commands) - @activationCommands[selector].push(commands) - else if _.isArray(commands) - @activationCommands[selector].push(commands...) - - @activationCommands - - subscribeToActivationHooks: -> - @activationHookSubscriptions = new CompositeDisposable - for hook in @getActivationHooks() - do (hook) => - @activationHookSubscriptions.add(@packageManager.onDidTriggerActivationHook(hook, => @activateNow())) if hook? and _.isString(hook) and hook.trim().length > 0 - - return - - getActivationHooks: -> - return @activationHooks if @metadata? and @activationHooks? - - @activationHooks = [] - - if @metadata.activationHooks? - if _.isArray(@metadata.activationHooks) - @activationHooks.push(@metadata.activationHooks...) - else if _.isString(@metadata.activationHooks) - @activationHooks.push(@metadata.activationHooks) - - @activationHooks = _.uniq(@activationHooks) - - getURIHandler: -> - @metadata?.uriHandler - - # Does the given module path contain native code? - isNativeModule: (modulePath) -> - try - fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node']).length > 0 - catch error - false - - # Get an array of all the native modules that this package depends on. - # - # First try to get this information from - # @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't - # exist, recurse through all dependencies. - getNativeModuleDependencyPaths: -> - nativeModulePaths = [] - - if @metadata._atomModuleCache? - relativeNativeModuleBindingPaths = @metadata._atomModuleCache.extensions?['.node'] ? [] - for relativeNativeModuleBindingPath in relativeNativeModuleBindingPaths - nativeModulePath = path.join(@path, relativeNativeModuleBindingPath, '..', '..', '..') - nativeModulePaths.push(nativeModulePath) - return nativeModulePaths - - traversePath = (nodeModulesPath) => - try - for modulePath in fs.listSync(nodeModulesPath) - nativeModulePaths.push(modulePath) if @isNativeModule(modulePath) - traversePath(path.join(modulePath, 'node_modules')) - return - - traversePath(path.join(@path, 'node_modules')) - 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: -> - return @compatible if @compatible? - - if @preloadedPackage - # Preloaded packages are always considered compatible - @compatible = true - else if @getMainModulePath() - @incompatibleModules = @getIncompatibleNativeModules() - @compatible = @incompatibleModules.length is 0 and not @getBuildFailureOutput()? - else - @compatible = true - - # 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: -> - new Promise (resolve) => - @runRebuildProcess (result) => - if result.code is 0 - global.localStorage.removeItem(@getBuildFailureOutputStorageKey()) - else - @compatible = false - global.localStorage.setItem(@getBuildFailureOutputStorageKey(), result.stderr) - global.localStorage.setItem(@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: -> - global.localStorage.getItem(@getBuildFailureOutputStorageKey()) - - runRebuildProcess: (callback) -> - stderr = '' - stdout = '' - new BufferedProcess({ - command: @packageManager.getApmPath() - args: ['rebuild', '--no-color'] - options: {cwd: @path} - stderr: (output) -> stderr += output - stdout: (output) -> stdout += output - exit: (code) -> callback({code, stdout, stderr}) - }) - - getBuildFailureOutputStorageKey: -> - "installed-packages:#{@name}:#{@metadata.version}:build-error" - - getIncompatibleNativeModulesStorageKey: -> - electronVersion = process.versions.electron - "installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules" - - getCanDeferMainModuleRequireStorageKey: -> - "installed-packages:#{@name}:#{@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: -> - unless @packageManager.devMode - try - if arrayAsString = global.localStorage.getItem(@getIncompatibleNativeModulesStorageKey()) - return JSON.parse(arrayAsString) - - incompatibleNativeModules = [] - for nativeModulePath in @getNativeModuleDependencyPaths() - try - require(nativeModulePath) - catch error - try - version = require("#{nativeModulePath}/package.json").version - incompatibleNativeModules.push - path: nativeModulePath - name: path.basename(nativeModulePath) - version: version - error: error.message - - global.localStorage.setItem(@getIncompatibleNativeModulesStorageKey(), JSON.stringify(incompatibleNativeModules)) - incompatibleNativeModules - - handleError: (message, error) -> - if atom.inSpecMode() - throw error - - if error.filename and error.location and (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} - at #{location} - """ - else if error.less and error.filename and error.column? and error.line? - # Less errors - location = "#{error.filename}:#{error.line}:#{error.column}" - detail = "#{error.message} in #{location}" - stack = """ - LessError: #{error.message} - at #{location} - """ - else - detail = error.message - stack = error.stack ? error - - @notificationManager.addFatalError(message, {stack, detail, packageName: @name, dismissable: true}) diff --git a/src/package.js b/src/package.js new file mode 100644 index 000000000..8d5cbc3ca --- /dev/null +++ b/src/package.js @@ -0,0 +1,1107 @@ +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 + }) + } +}