From 9732372e4fc093f272ba4327dc38f99ca4e1d7b3 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Fri, 7 Feb 2020 16:37:48 -0600 Subject: [PATCH] fix: remove atom-space-pen-view (#468) * clean up code * dispose disposables on deactivate * create client mock * add eslint rules * clean up eslint config * mergeFiles in client mock * remove semi * fix: remove atom-space-pen-view * remove inline style * clean up code * move require --- .eslintrc.js | 43 +- lib/config.js | 180 ++--- lib/fork-gistid-input-view.js | 93 ++- lib/package-manager.coffee | 469 +++++++++++++ lib/package-manager.js | 626 ----------------- lib/sync-settings.js | 1246 ++++++++++++++++----------------- package-lock.json | 89 +++ package.json | 3 +- release.config.js | 2 +- spec/create-client-mock.js | 102 +++ spec/spec-helpers.js | 51 -- spec/sync-settings-spec.js | 660 +++++++++-------- 12 files changed, 1733 insertions(+), 1831 deletions(-) create mode 100644 lib/package-manager.coffee delete mode 100644 lib/package-manager.js create mode 100644 spec/create-client-mock.js delete mode 100644 spec/spec-helpers.js diff --git a/.eslintrc.js b/.eslintrc.js index a3da9e44..f7ebc178 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,25 +1,22 @@ module.exports = { - env: { - browser: true, - commonjs: true, - es6: true, - node: true, - jasmine: true, - atomtest: true - }, - extends: [ - 'standard' - ], - globals: { - atom: 'readonly', - Atomics: 'readonly', - SharedArrayBuffer: 'readonly' - }, - parserOptions: { - ecmaVersion: 2018 - }, - rules: { - "handle-callback-err": "off", - camelcase: "off" - } + env: { + node: true, + jasmine: true, + atomtest: true, + }, + extends: [ + 'standard', + ], + globals: { + atom: 'readonly', + }, + parserOptions: { + ecmaVersion: 2018, + }, + rules: { + "no-warning-comments": "warn", + "comma-dangle": ["error", "always-multiline"], + indent: ["error", "tab"], + "no-tabs": ["error", { allowIndentationTabs: true }], + }, } diff --git a/lib/config.js b/lib/config.js index 24b15279..f3a90709 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,92 +1,92 @@ module.exports = { - personalAccessToken: { - description: 'Your personal GitHub access token', - type: 'string', - default: '', - order: 1 - }, - gistId: { - description: 'ID of gist to use for configuration storage', - type: 'string', - default: '', - order: 2 - }, - gistDescription: { - description: 'The description of the gist', - type: 'string', - default: 'automatic update by http://atom.io/packages/sync-settings', - order: 3 - }, - syncSettings: { - type: 'boolean', - default: true, - order: 4 - }, - blacklistedKeys: { - description: "Comma-seperated list of blacklisted keys (e.g. 'package-name,other-package-name.config-name')", - type: 'array', - default: [], - items: { - type: 'string' - }, - order: 5 - }, - syncPackages: { - type: 'boolean', - default: true, - order: 6 - }, - syncKeymap: { - type: 'boolean', - default: true, - order: 7 - }, - syncStyles: { - type: 'boolean', - default: true, - order: 8 - }, - syncInit: { - type: 'boolean', - default: true, - order: 9 - }, - syncSnippets: { - type: 'boolean', - default: true, - order: 10 - }, - extraFiles: { - description: 'Comma-seperated list of files other than Atom\'s default config files in ~/.atom', - type: 'array', - default: [], - items: { - type: 'string' - }, - order: 11 - }, - checkForUpdatedBackup: { - description: 'Check for newer backup on Atom start', - type: 'boolean', - default: true, - order: 12 - }, - _lastBackupHash: { - type: 'string', - default: '', - description: 'Hash of the last backup restored or created', - order: 13 - }, - quietUpdateCheck: { - type: 'boolean', - default: false, - description: "Mute 'Latest backup is already applied' message", - order: 14 - }, - removeObsoletePackages: { - description: 'Packages installed but not in the backup will be removed when restoring backups', - type: 'boolean', - default: false, - order: 15 - } + personalAccessToken: { + description: 'Your personal GitHub access token', + type: 'string', + default: '', + order: 1, + }, + gistId: { + description: 'ID of gist to use for configuration storage', + type: 'string', + default: '', + order: 2, + }, + gistDescription: { + description: 'The description of the gist', + type: 'string', + default: 'automatic update by http://atom.io/packages/sync-settings', + order: 3, + }, + syncSettings: { + type: 'boolean', + default: true, + order: 4, + }, + blacklistedKeys: { + description: "Comma-seperated list of blacklisted keys (e.g. 'package-name,other-package-name.config-name')", + type: 'array', + default: [], + items: { + type: 'string', + }, + order: 5, + }, + syncPackages: { + type: 'boolean', + default: true, + order: 6, + }, + syncKeymap: { + type: 'boolean', + default: true, + order: 7, + }, + syncStyles: { + type: 'boolean', + default: true, + order: 8, + }, + syncInit: { + type: 'boolean', + default: true, + order: 9, + }, + syncSnippets: { + type: 'boolean', + default: true, + order: 10, + }, + extraFiles: { + description: "Comma-seperated list of files other than Atom's default config files in ~/.atom", + type: 'array', + default: [], + items: { + type: 'string', + }, + order: 11, + }, + checkForUpdatedBackup: { + description: 'Check for newer backup on Atom start', + type: 'boolean', + default: true, + order: 12, + }, + _lastBackupHash: { + type: 'string', + default: '', + description: 'Hash of the last backup restored or created', + order: 13, + }, + quietUpdateCheck: { + type: 'boolean', + default: false, + description: "Mute 'Latest backup is already applied' message", + order: 14, + }, + removeObsoletePackages: { + description: 'Packages installed but not in the backup will be removed when restoring backups', + type: 'boolean', + default: false, + order: 15, + }, } diff --git a/lib/fork-gistid-input-view.js b/lib/fork-gistid-input-view.js index 04473459..37e3ae12 100644 --- a/lib/fork-gistid-input-view.js +++ b/lib/fork-gistid-input-view.js @@ -1,56 +1,47 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const { CompositeDisposable } = require('atom') -const { TextEditorView, View } = require('atom-space-pen-views') let oldView = null -module.exports = class ForkGistIdInputView extends View { - static content () { - return this.div({ class: 'command-palette' }, () => { - return this.subview('selectEditor', new TextEditorView({ mini: true, placeholderText: 'Gist ID to fork' })) - }) - } - - initialize () { - if (oldView != null) { - oldView.destroy() - } - oldView = this - - this.disposables = new CompositeDisposable() - this.disposables.add(atom.commands.add('atom-text-editor', 'core:confirm', () => this.confirm())) - this.disposables.add(atom.commands.add('atom-text-editor', 'core:cancel', () => this.destroy())) - return this.attach() - } - - destroy () { - this.disposables.dispose() - return this.detach() - } - - attach () { - if (this.panel == null) { this.panel = atom.workspace.addModalPanel({ item: this }) } - this.panel.show() - return this.selectEditor.focus() - } - - detach () { - this.panel.destroy() - return super.detach(...arguments) - } - - confirm () { - const gistId = this.selectEditor.getText() - this.callbackInstance.forkGistId(gistId) - return this.destroy() - } - - setCallbackInstance (callbackInstance) { - this.callbackInstance = callbackInstance - } +module.exports = class ForkGistIdInputView { + constructor (callbackInstance) { + if (oldView) { + oldView.destroy() + } + oldView = this + + this.disposables = new CompositeDisposable() + this.disposables.add( + atom.commands.add('atom-text-editor', 'core:confirm', this.confirm.bind(this)), + atom.commands.add('atom-text-editor', 'core:cancel', this.destroy.bind(this)), + ) + this.callbackInstance = callbackInstance + this.createElement() + + if (!this.panel) { + this.panel = atom.workspace.addModalPanel({ item: this }) + } + this.panel.show() + this.editor.focus() + } + + destroy () { + this.disposables.dispose() + this.panel.destroy() + } + + confirm () { + const gistId = this.editor.getText() + this.callbackInstance.forkGistId(gistId) + this.destroy() + } + + createElement () { + this.editor = document.createElement('atom-text-editor') + this.editor.setAttribute('mini', true) + this.editor.setAttribute('placeholder-text', 'Gist ID to fork') + + this.element = document.createElement('div') + this.element.classList.add('command-palette') + this.element.append(this.editor) + } } diff --git a/lib/package-manager.coffee b/lib/package-manager.coffee new file mode 100644 index 00000000..b9265b44 --- /dev/null +++ b/lib/package-manager.coffee @@ -0,0 +1,469 @@ +# modified from https://raw.githubusercontent.com/atom/settings-view/master/lib/package-manager.coffee +# modifications: commented out Client on line 8 + +_ = require 'underscore-plus' +{BufferedProcess, CompositeDisposable, Emitter} = require 'atom' +semver = require 'semver' + +# Client = require './atom-io-client' + +module.exports = +class PackageManager + # Millisecond expiry for cached loadOutdated, etc. values + CACHE_EXPIRY: 1000*60*10 + + constructor: -> + @packagePromises = [] + @apmCache = + loadOutdated: + value: null + expiry: 0 + + @emitter = new Emitter + + getClient: -> + @client ?= new Client(this) + + isPackageInstalled: (packageName) -> + if atom.packages.isPackageLoaded(packageName) + true + else + atom.packages.getAvailablePackageNames().indexOf(packageName) > -1 + + packageHasSettings: (packageName) -> + grammars = atom.grammars.getGrammars() ? [] + for grammar in grammars when grammar.path + return true if grammar.packageName is packageName + + pack = atom.packages.getLoadedPackage(packageName) + pack.activateConfig() if pack? and not atom.packages.isPackageActive(packageName) + schema = atom.config.getSchema(packageName) + schema? and (schema.type isnt 'any') + + setProxyServers: (callback) => + session = atom.getCurrentWindow().webContents.session + session.resolveProxy 'http://atom.io', (httpProxy) => + @applyProxyToEnv('http_proxy', httpProxy) + session.resolveProxy 'https://atom.io', (httpsProxy) => + @applyProxyToEnv('https_proxy', httpsProxy) + callback() + + setProxyServersAsync: (callback) => + httpProxyPromise = atom.resolveProxy('http://atom.io').then((proxy) => @applyProxyToEnv('http_proxy', proxy)) + httpsProxyPromise = atom.resolveProxy('https://atom.io').then((proxy) => @applyProxyToEnv('https_proxy', proxy)) + Promise.all([httpProxyPromise, httpsProxyPromise]).then(callback) + + applyProxyToEnv: (envName, proxy) -> + if proxy? + proxy = proxy.split(' ') + switch proxy[0].trim().toUpperCase() + when 'DIRECT' then delete process.env[envName] + when 'PROXY' then process.env[envName] = 'http://' + proxy[1] + return + + runCommand: (args, callback) -> + command = atom.packages.getApmPath() + outputLines = [] + stdout = (lines) -> outputLines.push(lines) + errorLines = [] + stderr = (lines) -> errorLines.push(lines) + exit = (code) -> + callback(code, outputLines.join('\n'), errorLines.join('\n')) + + args.push('--no-color') + + if atom.config.get('core.useProxySettingsWhenCallingApm') + bufferedProcess = new BufferedProcess({command, args, stdout, stderr, exit, autoStart: false}) + if atom.resolveProxy? + @setProxyServersAsync -> bufferedProcess.start() + else + @setProxyServers -> bufferedProcess.start() + return bufferedProcess + else + return new BufferedProcess({command, args, stdout, stderr, exit}) + + loadInstalled: (callback) -> + args = ['ls', '--json'] + errorMessage = 'Fetching local packages failed.' + apmProcess = @runCommand args, (code, stdout, stderr) -> + if code is 0 + try + packages = JSON.parse(stdout) ? [] + catch parseError + error = createJsonParseError(errorMessage, parseError, stdout) + return callback(error) + callback(null, packages) + else + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + callback(error) + + handleProcessErrors(apmProcess, errorMessage, callback) + + loadFeatured: (loadThemes, callback) -> + unless callback + callback = loadThemes + loadThemes = false + + args = ['featured', '--json'] + version = atom.getVersion() + args.push('--themes') if loadThemes + args.push('--compatible', version) if semver.valid(version) + errorMessage = 'Fetching featured packages failed.' + + apmProcess = @runCommand args, (code, stdout, stderr) -> + if code is 0 + try + packages = JSON.parse(stdout) ? [] + catch parseError + error = createJsonParseError(errorMessage, parseError, stdout) + return callback(error) + + callback(null, packages) + else + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + callback(error) + + handleProcessErrors(apmProcess, errorMessage, callback) + + loadOutdated: (clearCache, callback) -> + if clearCache + @clearOutdatedCache() + # Short circuit if we have cached data. + else if @apmCache.loadOutdated.value and @apmCache.loadOutdated.expiry > Date.now() + return callback(null, @apmCache.loadOutdated.value) + + args = ['outdated', '--json'] + version = atom.getVersion() + args.push('--compatible', version) if semver.valid(version) + errorMessage = 'Fetching outdated packages and themes failed.' + + apmProcess = @runCommand args, (code, stdout, stderr) => + if code is 0 + try + packages = JSON.parse(stdout) ? [] + catch parseError + error = createJsonParseError(errorMessage, parseError, stdout) + return callback(error) + + updatablePackages = (pack for pack in packages when not @getVersionPinnedPackages().includes(pack?.name)) + + @apmCache.loadOutdated = + value: updatablePackages + expiry: Date.now() + @CACHE_EXPIRY + + for pack in updatablePackages + @emitPackageEvent 'update-available', pack + + callback(null, updatablePackages) + else + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + callback(error) + + handleProcessErrors(apmProcess, errorMessage, callback) + + getVersionPinnedPackages: -> + atom.config.get('core.versionPinnedPackages') ? [] + + clearOutdatedCache: -> + @apmCache.loadOutdated = + value: null + expiry: 0 + + loadPackage: (packageName, callback) -> + args = ['view', packageName, '--json'] + errorMessage = "Fetching package '#{packageName}' failed." + + apmProcess = @runCommand args, (code, stdout, stderr) -> + if code is 0 + try + packages = JSON.parse(stdout) ? [] + catch parseError + error = createJsonParseError(errorMessage, parseError, stdout) + return callback(error) + + callback(null, packages) + else + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + callback(error) + + handleProcessErrors(apmProcess, errorMessage, callback) + + loadCompatiblePackageVersion: (packageName, callback) -> + args = ['view', packageName, '--json', '--compatible', @normalizeVersion(atom.getVersion())] + errorMessage = "Fetching package '#{packageName}' failed." + + apmProcess = @runCommand args, (code, stdout, stderr) -> + if code is 0 + try + packages = JSON.parse(stdout) ? [] + catch parseError + error = createJsonParseError(errorMessage, parseError, stdout) + return callback(error) + + callback(null, packages) + else + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + callback(error) + + handleProcessErrors(apmProcess, errorMessage, callback) + + getInstalled: -> + new Promise (resolve, reject) => + @loadInstalled (error, result) -> + if error + reject(error) + else + resolve(result) + + getFeatured: (loadThemes) -> + new Promise (resolve, reject) => + @loadFeatured !!loadThemes, (error, result) -> + if error + reject(error) + else + resolve(result) + + getOutdated: (clearCache = false) -> + new Promise (resolve, reject) => + @loadOutdated clearCache, (error, result) -> + if error + reject(error) + else + resolve(result) + + getPackage: (packageName) -> + @packagePromises[packageName] ?= new Promise (resolve, reject) => + @loadPackage packageName, (error, result) -> + if error + reject(error) + else + resolve(result) + + satisfiesVersion: (version, metadata) -> + engine = metadata.engines?.atom ? '*' + return false unless semver.validRange(engine) + return semver.satisfies(version, engine) + + normalizeVersion: (version) -> + [version] = version.split('-') if typeof version is 'string' + version + + update: (pack, newVersion, callback) -> + {name, theme, apmInstallSource} = pack + + errorMessage = if newVersion + "Updating to \u201C#{name}@#{newVersion}\u201D failed." + else + "Updating to latest sha failed." + onError = (error) => + error.packageInstallError = not theme + @emitPackageEvent 'update-failed', pack, error + callback?(error) + + if apmInstallSource?.type is 'git' + args = ['install', apmInstallSource.source] + else + args = ['install', "#{name}@#{newVersion}"] + + exit = (code, stdout, stderr) => + if code is 0 + @clearOutdatedCache() + callback?() + @emitPackageEvent 'updated', pack + else + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + onError(error) + + @emitPackageEvent 'updating', pack + apmProcess = @runCommand(args, exit) + handleProcessErrors(apmProcess, errorMessage, onError) + + unload: (name) -> + if atom.packages.isPackageLoaded(name) + atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name) + atom.packages.unloadPackage(name) + + install: (pack, callback) -> + {name, version, theme} = pack + activateOnSuccess = not theme and not atom.packages.isPackageDisabled(name) + activateOnFailure = atom.packages.isPackageActive(name) + nameWithVersion = if version? then "#{name}@#{version}" else name + + @unload(name) + args = ['install', nameWithVersion, '--json'] + + errorMessage = "Installing \u201C#{nameWithVersion}\u201D failed." + onError = (error) => + error.packageInstallError = not theme + @emitPackageEvent 'install-failed', pack, error + callback?(error) + + exit = (code, stdout, stderr) => + if code is 0 + # get real package name from package.json + try + packageInfo = JSON.parse(stdout)[0] + pack = _.extend({}, pack, packageInfo.metadata) + name = pack.name + catch err + # using old apm without --json support + @clearOutdatedCache() + if activateOnSuccess + atom.packages.activatePackage(name) + else + atom.packages.loadPackage(name) + + callback?() + @emitPackageEvent 'installed', pack + else + atom.packages.activatePackage(name) if activateOnFailure + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + onError(error) + + @emitPackageEvent('installing', pack) + apmProcess = @runCommand(args, exit) + handleProcessErrors(apmProcess, errorMessage, onError) + + uninstall: (pack, callback) -> + {name} = pack + + atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name) + + errorMessage = "Uninstalling \u201C#{name}\u201D failed." + onError = (error) => + @emitPackageEvent 'uninstall-failed', pack, error + callback?(error) + + @emitPackageEvent('uninstalling', pack) + apmProcess = @runCommand ['uninstall', '--hard', name], (code, stdout, stderr) => + if code is 0 + @clearOutdatedCache() + @unload(name) + @removePackageNameFromDisabledPackages(name) + callback?() + @emitPackageEvent 'uninstalled', pack + else + error = new Error(errorMessage) + error.stdout = stdout + error.stderr = stderr + onError(error) + + handleProcessErrors(apmProcess, errorMessage, onError) + + installAlternative: (pack, alternativePackageName, callback) -> + eventArg = {pack, alternative: alternativePackageName} + @emitter.emit('package-installing-alternative', eventArg) + + uninstallPromise = new Promise (resolve, reject) => + @uninstall pack, (error) -> + if error then reject(error) else resolve() + + installPromise = new Promise (resolve, reject) => + @install {name: alternativePackageName}, (error) -> + if error then reject(error) else resolve() + + Promise.all([uninstallPromise, installPromise]).then => + callback(null, eventArg) + @emitter.emit('package-installed-alternative', eventArg) + .catch (error) => + console.error error.message, error.stack + callback(error, eventArg) + eventArg.error = error + @emitter.emit('package-install-alternative-failed', eventArg) + + canUpgrade: (installedPackage, availableVersion) -> + return false unless installedPackage? + + installedVersion = installedPackage.metadata.version + return false unless semver.valid(installedVersion) + return false unless semver.valid(availableVersion) + + semver.gt(availableVersion, installedVersion) + + getPackageTitle: ({name}) -> + _.undasherize(_.uncamelcase(name)) + + getRepositoryUrl: ({metadata}) -> + {repository} = metadata + repoUrl = repository?.url ? repository ? '' + if repoUrl.match 'git@github' + repoName = repoUrl.split(':')[1] + repoUrl = "https://github.com/#{repoName}" + repoUrl.replace(/\.git$/, '').replace(/\/+$/, '').replace(/^git\+/, '') + + getRepositoryBugUri: ({metadata}) -> + {bugs} = metadata + if typeof bugs is 'string' + bugUri = bugs + else + bugUri = bugs?.url ? bugs?.email ? this.getRepositoryUrl({metadata}) + '/issues/new' + if bugUri.includes('@') + bugUri = 'mailto:' + bugUri + bugUri + + checkNativeBuildTools: -> + new Promise (resolve, reject) => + apmProcess = @runCommand ['install', '--check'], (code, stdout, stderr) -> + if code is 0 + resolve() + else + reject(new Error()) + + apmProcess.onWillThrowError ({error, handle}) -> + handle() + reject(error) + + removePackageNameFromDisabledPackages: (packageName) -> + atom.config.removeAtKeyPath('core.disabledPackages', packageName) + + # Emits the appropriate event for the given package. + # + # All events are either of the form `theme-foo` or `package-foo` depending on + # whether the event is for a theme or a normal package. This method standardizes + # the logic to determine if a package is a theme or not and formats the event + # name appropriately. + # + # eventName - The event name suffix {String} of the event to emit. + # pack - The package for which the event is being emitted. + # error - Any error information to be included in the case of an error. + emitPackageEvent: (eventName, pack, error) -> + theme = pack.theme ? pack.metadata?.theme + eventName = if theme then "theme-#{eventName}" else "package-#{eventName}" + @emitter.emit(eventName, {pack, error}) + + on: (selectors, callback) -> + subscriptions = new CompositeDisposable + for selector in selectors.split(" ") + subscriptions.add @emitter.on(selector, callback) + subscriptions + +createJsonParseError = (message, parseError, stdout) -> + error = new Error(message) + error.stdout = '' + error.stderr = "#{parseError.message}: #{stdout}" + error + +createProcessError = (message, processError) -> + error = new Error(message) + error.stdout = '' + error.stderr = processError.message + error + +handleProcessErrors = (apmProcess, message, callback) -> + apmProcess.onWillThrowError ({error, handle}) -> + handle() + callback(createProcessError(message, error)) diff --git a/lib/package-manager.js b/lib/package-manager.js deleted file mode 100644 index 8ab8fe82..00000000 --- a/lib/package-manager.js +++ /dev/null @@ -1,626 +0,0 @@ -// modified from https://github.com/atom/settings-view/blob/master/lib/package-manager.coffee -// removed Client dependency - -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS205: Consider reworking code to avoid use of IIFEs - * DS206: Consider reworking classes to avoid initClass - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -let PackageManager -const _ = require('underscore-plus') -const { BufferedProcess, CompositeDisposable, Emitter } = require('atom') -const semver = require('semver') - -module.exports = -(PackageManager = (function () { - PackageManager = class PackageManager { - static initClass () { - // Millisecond expiry for cached loadOutdated, etc. values - this.prototype.CACHE_EXPIRY = 1000 * 60 * 10 - } - - constructor () { - this.setProxyServers = this.setProxyServers.bind(this) - this.setProxyServersAsync = this.setProxyServersAsync.bind(this) - this.packagePromises = [] - this.apmCache = { - loadOutdated: { - value: null, - expiry: 0 - } - } - - this.emitter = new Emitter() - } - - isPackageInstalled (packageName) { - if (atom.packages.isPackageLoaded(packageName)) { - return true - } else { - return atom.packages.getAvailablePackageNames().indexOf(packageName) > -1 - } - } - - packageHasSettings (packageName) { - let left - const grammars = (left = atom.grammars.getGrammars()) != null ? left : [] - for (const grammar of Array.from(grammars)) { - if (grammar.path) { - if (grammar.packageName === packageName) { return true } - } - } - - const pack = atom.packages.getLoadedPackage(packageName) - if ((pack != null) && !atom.packages.isPackageActive(packageName)) { pack.activateConfig() } - const schema = atom.config.getSchema(packageName) - return (schema != null) && (schema.type !== 'any') - } - - setProxyServers (callback) { - const { - session - } = atom.getCurrentWindow().webContents - return session.resolveProxy('http://atom.io', httpProxy => { - this.applyProxyToEnv('http_proxy', httpProxy) - return session.resolveProxy('https://atom.io', httpsProxy => { - this.applyProxyToEnv('https_proxy', httpsProxy) - return callback() - }) - }) - } - - setProxyServersAsync (callback) { - const httpProxyPromise = atom.resolveProxy('http://atom.io').then(proxy => this.applyProxyToEnv('http_proxy', proxy)) - const httpsProxyPromise = atom.resolveProxy('https://atom.io').then(proxy => this.applyProxyToEnv('https_proxy', proxy)) - return Promise.all([httpProxyPromise, httpsProxyPromise]).then(callback) - } - - applyProxyToEnv (envName, proxy) { - if (proxy != null) { - proxy = proxy.split(' ') - switch (proxy[0].trim().toUpperCase()) { - case 'DIRECT': delete process.env[envName]; break - case 'PROXY': process.env[envName] = 'http://' + proxy[1]; break - } - } - } - - runCommand (args, callback) { - const command = atom.packages.getApmPath() - const outputLines = [] - const stdout = lines => outputLines.push(lines) - const errorLines = [] - const stderr = lines => errorLines.push(lines) - const exit = code => callback(code, outputLines.join('\n'), errorLines.join('\n')) - - args.push('--no-color') - - if (atom.config.get('core.useProxySettingsWhenCallingApm')) { - const bufferedProcess = new BufferedProcess({ command, args, stdout, stderr, exit, autoStart: false }) - if (atom.resolveProxy != null) { - this.setProxyServersAsync(() => bufferedProcess.start()) - } else { - this.setProxyServers(() => bufferedProcess.start()) - } - return bufferedProcess - } else { - return new BufferedProcess({ command, args, stdout, stderr, exit }) - } - } - - loadInstalled (callback) { - const args = ['ls', '--json'] - const errorMessage = 'Fetching local packages failed.' - const apmProcess = this.runCommand(args, function (code, stdout, stderr) { - let error - if (code === 0) { - let packages - try { - let left - packages = (left = JSON.parse(stdout)) != null ? left : [] - } catch (parseError) { - error = createJsonParseError(errorMessage, parseError, stdout) - return callback(error) - } - return callback(null, packages) - } else { - error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return callback(error) - } - }) - - return handleProcessErrors(apmProcess, errorMessage, callback) - } - - loadFeatured (loadThemes, callback) { - if (!callback) { - callback = loadThemes - loadThemes = false - } - - const args = ['featured', '--json'] - const version = atom.getVersion() - if (loadThemes) { args.push('--themes') } - if (semver.valid(version)) { args.push('--compatible', version) } - const errorMessage = 'Fetching featured packages failed.' - - const apmProcess = this.runCommand(args, function (code, stdout, stderr) { - let error - if (code === 0) { - let packages - try { - let left - packages = (left = JSON.parse(stdout)) != null ? left : [] - } catch (parseError) { - error = createJsonParseError(errorMessage, parseError, stdout) - return callback(error) - } - - return callback(null, packages) - } else { - error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return callback(error) - } - }) - - return handleProcessErrors(apmProcess, errorMessage, callback) - } - - loadOutdated (clearCache, callback) { - if (clearCache) { - this.clearOutdatedCache() - // Short circuit if we have cached data. - } else if (this.apmCache.loadOutdated.value && (this.apmCache.loadOutdated.expiry > Date.now())) { - return callback(null, this.apmCache.loadOutdated.value) - } - - const args = ['outdated', '--json'] - const version = atom.getVersion() - if (semver.valid(version)) { args.push('--compatible', version) } - const errorMessage = 'Fetching outdated packages and themes failed.' - - const apmProcess = this.runCommand(args, (code, stdout, stderr) => { - let error - let pack - if (code === 0) { - let packages - try { - let left - packages = (left = JSON.parse(stdout)) != null ? left : [] - } catch (parseError) { - error = createJsonParseError(errorMessage, parseError, stdout) - return callback(error) - } - - const updatablePackages = ((() => { - const result = [] - for (pack of Array.from(packages)) { - if (!this.getVersionPinnedPackages().includes(pack != null ? pack.name : undefined)) { - result.push(pack) - } - } - return result - })()) - - this.apmCache.loadOutdated = { - value: updatablePackages, - expiry: Date.now() + this.CACHE_EXPIRY - } - - for (pack of Array.from(updatablePackages)) { - this.emitPackageEvent('update-available', pack) - } - - return callback(null, updatablePackages) - } else { - error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return callback(error) - } - }) - - return handleProcessErrors(apmProcess, errorMessage, callback) - } - - getVersionPinnedPackages () { - let left - return (left = atom.config.get('core.versionPinnedPackages')) != null ? left : [] - } - - clearOutdatedCache () { - this.apmCache.loadOutdated = { - value: null, - expiry: 0 - } - } - - loadPackage (packageName, callback) { - const args = ['view', packageName, '--json'] - const errorMessage = `Fetching package '${packageName}' failed.` - - const apmProcess = this.runCommand(args, function (code, stdout, stderr) { - let error - if (code === 0) { - let packages - try { - let left - packages = (left = JSON.parse(stdout)) != null ? left : [] - } catch (parseError) { - error = createJsonParseError(errorMessage, parseError, stdout) - return callback(error) - } - - return callback(null, packages) - } else { - error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return callback(error) - } - }) - - return handleProcessErrors(apmProcess, errorMessage, callback) - } - - loadCompatiblePackageVersion (packageName, callback) { - const args = ['view', packageName, '--json', '--compatible', this.normalizeVersion(atom.getVersion())] - const errorMessage = `Fetching package '${packageName}' failed.` - - const apmProcess = this.runCommand(args, function (code, stdout, stderr) { - let error - if (code === 0) { - let packages - try { - let left - packages = (left = JSON.parse(stdout)) != null ? left : [] - } catch (parseError) { - error = createJsonParseError(errorMessage, parseError, stdout) - return callback(error) - } - - return callback(null, packages) - } else { - error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return callback(error) - } - }) - - return handleProcessErrors(apmProcess, errorMessage, callback) - } - - getInstalled () { - return new Promise((resolve, reject) => { - return this.loadInstalled(function (error, result) { - if (error) { - return reject(error) - } else { - return resolve(result) - } - }) - }) - } - - getFeatured (loadThemes) { - return new Promise((resolve, reject) => { - return this.loadFeatured(!!loadThemes, function (error, result) { - if (error) { - return reject(error) - } else { - return resolve(result) - } - }) - }) - } - - getOutdated (clearCache) { - if (clearCache == null) { clearCache = false } - return new Promise((resolve, reject) => { - return this.loadOutdated(clearCache, function (error, result) { - if (error) { - return reject(error) - } else { - return resolve(result) - } - }) - }) - } - - getPackage (packageName) { - return this.packagePromises[packageName] != null ? this.packagePromises[packageName] : (this.packagePromises[packageName] = new Promise((resolve, reject) => { - return this.loadPackage(packageName, function (error, result) { - if (error) { - return reject(error) - } else { - return resolve(result) - } - }) - })) - } - - satisfiesVersion (version, metadata) { - const engine = (metadata.engines != null ? metadata.engines.atom : undefined) != null ? (metadata.engines != null ? metadata.engines.atom : undefined) : '*' - if (!semver.validRange(engine)) { return false } - return semver.satisfies(version, engine) - } - - normalizeVersion (version) { - if (typeof version === 'string') { [version] = Array.from(version.split('-')) } - return version - } - - update (pack, newVersion, callback) { - let args - const { name, theme, apmInstallSource } = pack - - const errorMessage = newVersion - ? `Updating to \u201C${name}@${newVersion}\u201D failed.` - : 'Updating to latest sha failed.' - const onError = error => { - error.packageInstallError = !theme - this.emitPackageEvent('update-failed', pack, error) - return (typeof callback === 'function' ? callback(error) : undefined) - } - - if ((apmInstallSource != null ? apmInstallSource.type : undefined) === 'git') { - args = ['install', apmInstallSource.source] - } else { - args = ['install', `${name}@${newVersion}`] - } - - const exit = (code, stdout, stderr) => { - if (code === 0) { - this.clearOutdatedCache() - if (typeof callback === 'function') { - callback() - } - return this.emitPackageEvent('updated', pack) - } else { - const error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return onError(error) - } - } - - this.emitPackageEvent('updating', pack) - const apmProcess = this.runCommand(args, exit) - return handleProcessErrors(apmProcess, errorMessage, onError) - } - - unload (name) { - if (atom.packages.isPackageLoaded(name)) { - if (atom.packages.isPackageActive(name)) { atom.packages.deactivatePackage(name) } - return atom.packages.unloadPackage(name) - } - } - - install (pack, callback) { - let { name, version, theme } = pack - const activateOnSuccess = !theme && !atom.packages.isPackageDisabled(name) - const activateOnFailure = atom.packages.isPackageActive(name) - const nameWithVersion = (version != null) ? `${name}@${version}` : name - - this.unload(name) - const args = ['install', nameWithVersion, '--json'] - - const errorMessage = `Installing \u201C${nameWithVersion}\u201D failed.` - const onError = error => { - error.packageInstallError = !theme - this.emitPackageEvent('install-failed', pack, error) - return (typeof callback === 'function' ? callback(error) : undefined) - } - - const exit = (code, stdout, stderr) => { - if (code === 0) { - // get real package name from package.json - try { - const packageInfo = JSON.parse(stdout)[0] - pack = _.extend({}, pack, packageInfo.metadata); - ({ - name - } = pack) - } catch (err) {} - // using old apm without --json support - this.clearOutdatedCache() - if (activateOnSuccess) { - atom.packages.activatePackage(name) - } else { - atom.packages.loadPackage(name) - } - - if (typeof callback === 'function') { - callback() - } - return this.emitPackageEvent('installed', pack) - } else { - if (activateOnFailure) { atom.packages.activatePackage(name) } - const error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return onError(error) - } - } - - this.emitPackageEvent('installing', pack) - const apmProcess = this.runCommand(args, exit) - return handleProcessErrors(apmProcess, errorMessage, onError) - } - - uninstall (pack, callback) { - const { name } = pack - - if (atom.packages.isPackageActive(name)) { atom.packages.deactivatePackage(name) } - - const errorMessage = `Uninstalling \u201C${name}\u201D failed.` - const onError = error => { - this.emitPackageEvent('uninstall-failed', pack, error) - return (typeof callback === 'function' ? callback(error) : undefined) - } - - this.emitPackageEvent('uninstalling', pack) - const apmProcess = this.runCommand(['uninstall', '--hard', name], (code, stdout, stderr) => { - if (code === 0) { - this.clearOutdatedCache() - this.unload(name) - this.removePackageNameFromDisabledPackages(name) - if (typeof callback === 'function') { - callback() - } - return this.emitPackageEvent('uninstalled', pack) - } else { - const error = new Error(errorMessage) - error.stdout = stdout - error.stderr = stderr - return onError(error) - } - }) - - return handleProcessErrors(apmProcess, errorMessage, onError) - } - - installAlternative (pack, alternativePackageName, callback) { - const eventArg = { pack, alternative: alternativePackageName } - this.emitter.emit('package-installing-alternative', eventArg) - - const uninstallPromise = new Promise((resolve, reject) => { - return this.uninstall(pack, function (error) { - if (error) { return reject(error) } else { return resolve() } - }) - }) - - const installPromise = new Promise((resolve, reject) => { - return this.install({ name: alternativePackageName }, function (error) { - if (error) { return reject(error) } else { return resolve() } - }) - }) - - return Promise.all([uninstallPromise, installPromise]).then(() => { - callback(null, eventArg) - return this.emitter.emit('package-installed-alternative', eventArg) - }).catch(error => { - console.error(error.message, error.stack) - callback(error, eventArg) - eventArg.error = error - return this.emitter.emit('package-install-alternative-failed', eventArg) - }) - } - - canUpgrade (installedPackage, availableVersion) { - if (installedPackage == null) { return false } - - const installedVersion = installedPackage.metadata.version - if (!semver.valid(installedVersion)) { return false } - if (!semver.valid(availableVersion)) { return false } - - return semver.gt(availableVersion, installedVersion) - } - - getPackageTitle ({ name }) { - return _.undasherize(_.uncamelcase(name)) - } - - getRepositoryUrl ({ metadata }) { - let left - const { repository } = metadata - let repoUrl = (left = (repository != null ? repository.url : undefined) != null ? (repository != null ? repository.url : undefined) : repository) != null ? left : '' - if (repoUrl.match('git@github')) { - const repoName = repoUrl.split(':')[1] - repoUrl = `https://github.com/${repoName}` - } - return repoUrl.replace(/\.git$/, '').replace(/\/+$/, '').replace(/^git\+/, '') - } - - getRepositoryBugUri ({ metadata }) { - let bugUri - const { bugs } = metadata - if (typeof bugs === 'string') { - bugUri = bugs - } else { - let left - bugUri = (left = (bugs != null ? bugs.url : undefined) != null ? (bugs != null ? bugs.url : undefined) : (bugs != null ? bugs.email : undefined)) != null ? left : this.getRepositoryUrl({ metadata }) + '/issues/new' - if (bugUri.includes('@')) { - bugUri = 'mailto:' + bugUri - } - } - return bugUri - } - - checkNativeBuildTools () { - return new Promise((resolve, reject) => { - const apmProcess = this.runCommand(['install', '--check'], function (code, stdout, stderr) { - if (code === 0) { - return resolve() - } else { - return reject(new Error()) - } - }) - - return apmProcess.onWillThrowError(function ({ error, handle }) { - handle() - return reject(error) - }) - }) - } - - removePackageNameFromDisabledPackages (packageName) { - return atom.config.removeAtKeyPath('core.disabledPackages', packageName) - } - - // Emits the appropriate event for the given package. - // - // All events are either of the form `theme-foo` or `package-foo` depending on - // whether the event is for a theme or a normal package. This method standardizes - // the logic to determine if a package is a theme or not and formats the event - // name appropriately. - // - // eventName - The event name suffix {String} of the event to emit. - // pack - The package for which the event is being emitted. - // error - Any error information to be included in the case of an error. - emitPackageEvent (eventName, pack, error) { - const theme = pack.theme != null ? pack.theme : (pack.metadata != null ? pack.metadata.theme : undefined) - eventName = theme ? `theme-${eventName}` : `package-${eventName}` - return this.emitter.emit(eventName, { pack, error }) - } - - on (selectors, callback) { - const subscriptions = new CompositeDisposable() - for (const selector of Array.from(selectors.split(' '))) { - subscriptions.add(this.emitter.on(selector, callback)) - } - return subscriptions - } - } - PackageManager.initClass() - return PackageManager -})()) - -var createJsonParseError = function (message, parseError, stdout) { - const error = new Error(message) - error.stdout = '' - error.stderr = `${parseError.message}: ${stdout}` - return error -} - -const createProcessError = function (message, processError) { - const error = new Error(message) - error.stdout = '' - error.stderr = processError.message - return error -} - -var handleProcessErrors = (apmProcess, message, callback) => apmProcess.onWillThrowError(function ({ error, handle }) { - handle() - return callback(createProcessError(message, error)) -}) diff --git a/lib/sync-settings.js b/lib/sync-settings.js index a5d4da5f..362f1b02 100644 --- a/lib/sync-settings.js +++ b/lib/sync-settings.js @@ -1,651 +1,615 @@ -/* - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS103: Rewrite code to no longer use __guard__ - * DS104: Avoid inline assignments - * DS202: Simplify dynamic range loops - * DS203: Remove `|| {}` from converted for-own loops - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ // imports const fs = require('fs') +const util = require('util') +const writeFile = util.promisify(fs.writeFile) +const readFile = util.promisify(fs.readFile) const _ = require('underscore-plus') -let GitHubApi, PackageManager -let ForkGistIdInputView = null +let GitHubApi +let PackageManager +let ForkGistIdInputView // constants // const DESCRIPTION = 'Atom configuration storage operated by http://atom.io/packages/sync-settings' const REMOVE_KEYS = [ - 'sync-settings.gistId', - 'sync-settings.personalAccessToken', - 'sync-settings._analyticsUserId', // keep legacy key in blacklist - 'sync-settings._lastBackupHash' + 'sync-settings.gistId', + 'sync-settings.personalAccessToken', + 'sync-settings._analyticsUserId', // keep legacy key in blacklist + 'sync-settings._lastBackupHash', ] -const SyncSettings = { - config: require('./config.js'), - - activate () { - // speedup activation by async initializing - return setImmediate(() => { - // actual initialization after atom has loaded - if (GitHubApi == null) { GitHubApi = require('@octokit/rest') } - if (PackageManager == null) { PackageManager = require('./package-manager') } - - atom.commands.add('atom-workspace', 'sync-settings:backup', () => { - return this.backup() - }) - atom.commands.add('atom-workspace', 'sync-settings:restore', () => { - return this.restore() - }) - atom.commands.add('atom-workspace', 'sync-settings:view-backup', () => { - return this.viewBackup() - }) - atom.commands.add('atom-workspace', 'sync-settings:check-backup', () => { - return this.checkForUpdate() - }) - atom.commands.add('atom-workspace', 'sync-settings:fork', () => { - return this.inputForkGistId() - }) - - const mandatorySettingsApplied = this.checkMandatorySettings() - if (atom.config.get('sync-settings.checkForUpdatedBackup') && mandatorySettingsApplied) { return this.checkForUpdate() } - }) - }, - - deactivate () { - return (this.inputView != null ? this.inputView.destroy() : undefined) - }, - - serialize () {}, - - getGistId () { - let gistId = atom.config.get('sync-settings.gistId') || process.env.GIST_ID - if (gistId) { - gistId = gistId.trim() - } - return gistId - }, - - getPersonalAccessToken () { - let token = atom.config.get('sync-settings.personalAccessToken') || process.env.GITHUB_TOKEN - if (token) { - token = token.trim() - } - return token - }, - - checkMandatorySettings () { - const missingSettings = [] - if (!this.getGistId()) { - missingSettings.push('Gist ID') - } - if (!this.getPersonalAccessToken()) { - missingSettings.push('GitHub personal access token') - } - if (missingSettings.length) { - this.notifyMissingMandatorySettings(missingSettings) - } - return missingSettings.length === 0 - }, - - checkForUpdate (cb = null) { - if (this.getGistId()) { - console.log('checking latest backup...') - return this.createClient().gists.get({ - gist_id: this.getGistId() - }).then(res => { - if ((__guard__(__guard__(res != null ? res.data.history : undefined, x1 => x1[0]), x => x.version) == null)) { - console.error('could not interpret result:', res) - atom.notifications.addError('sync-settings: Error retrieving your settings.') - return (typeof cb === 'function' ? cb() : undefined) - } - - console.log(`latest backup version ${res.data.history[0].version}`) - if (res.data.history[0].version !== atom.config.get('sync-settings._lastBackupHash')) { - this.notifyNewerBackup() - } else if (!atom.config.get('sync-settings.quietUpdateCheck')) { - this.notifyBackupUptodate() - } - - return (typeof cb === 'function' ? cb() : undefined) - }).catch(err => { - let message - console.error('error while retrieving the gist. does it exists?', err) - try { - ({ - message - } = JSON.parse(err.message)) - if (message === 'Not Found') { message = 'Gist ID Not Found' } - } catch (SyntaxError) { - ({ - message - } = err) - } - atom.notifications.addError('sync-settings: Error retrieving your settings. (' + message + ')') - return (typeof cb === 'function' ? cb() : undefined) - }) - } else { - return this.notifyMissingMandatorySettings(['Gist ID']) - } - }, - - notifyNewerBackup () { - // we need the actual element for dispatching on it - const workspaceElement = atom.views.getView(atom.workspace) - const notification = atom.notifications.addWarning('sync-settings: Your settings are out of date.', { - dismissable: true, - buttons: [{ - text: 'Backup', - onDidClick () { - atom.commands.dispatch(workspaceElement, 'sync-settings:backup') - return notification.dismiss() - } - }, { - text: 'View backup', - onDidClick () { - return atom.commands.dispatch(workspaceElement, 'sync-settings:view-backup') - } - }, { - text: 'Restore', - onDidClick () { - atom.commands.dispatch(workspaceElement, 'sync-settings:restore') - return notification.dismiss() - } - }, { - text: 'Dismiss', - onDidClick () { return notification.dismiss() } - }] - }) - }, - - notifyBackupUptodate () { - return atom.notifications.addSuccess('sync-settings: Latest backup is already applied.') - }, - - notifyMissingMandatorySettings (missingSettings) { - const context = this - const errorMsg = 'sync-settings: Mandatory settings missing: ' + missingSettings.join(', ') - - const notification = atom.notifications.addError(errorMsg, { - dismissable: true, - buttons: [{ - text: 'Package settings', - onDidClick () { - context.goToPackageSettings() - return notification.dismiss() - } - }] - }) - }, - - backup (cb = null) { - let left4 - const files = {} - if (atom.config.get('sync-settings.syncSettings')) { - files['settings.json'] = { content: this.getFilteredSettings() } - } - if (atom.config.get('sync-settings.syncPackages')) { - files['packages.json'] = { content: JSON.stringify(this.getPackages(), null, '\t') } - } - if (atom.config.get('sync-settings.syncKeymap')) { - let left - files['keymap.cson'] = { content: ((left = this.fileContent(atom.keymaps.getUserKeymapPath()))) != null ? left : '# keymap file (not found)' } - } - if (atom.config.get('sync-settings.syncStyles')) { - let left1 - files['styles.less'] = { content: ((left1 = this.fileContent(atom.styles.getUserStyleSheetPath()))) != null ? left1 : '// styles file (not found)' } - } - if (atom.config.get('sync-settings.syncInit')) { - let left2 - const initPath = atom.getUserInitScriptPath() - const path = require('path') - files[path.basename(initPath)] = { content: ((left2 = this.fileContent(initPath))) != null ? left2 : '# initialization file (not found)' } - } - if (atom.config.get('sync-settings.syncSnippets')) { - let left3 - files['snippets.cson'] = { content: ((left3 = this.fileContent(atom.getConfigDirPath() + '/snippets.cson'))) != null ? left3 : '# snippets file (not found)' } - } - - for (const file of Array.from((left4 = atom.config.get('sync-settings.extraFiles')) != null ? left4 : [])) { - var left5 - const ext = file.slice(file.lastIndexOf('.')).toLowerCase() - let cmtstart = '#' - if (['.less', '.scss', '.js'].includes(ext)) { cmtstart = '//' } - if (['.css'].includes(ext)) { cmtstart = '/*' } - let cmtend = '' - if (['.css'].includes(ext)) { cmtend = '*/' } - files[file] = - { content: ((left5 = this.fileContent(atom.getConfigDirPath() + `/${file}`))) != null ? left5 : `${cmtstart} ${file} (not found) ${cmtend}` } - } - - return this.createClient().gists.update({ - gist_id: this.getGistId(), - description: atom.config.get('sync-settings.gistDescription'), - files - }).then(res => { - atom.config.set('sync-settings._lastBackupHash', res.data.history[0].version) - atom.notifications.addSuccess("sync-settings: Your settings were successfully backed up.
Click here to open your Gist.") - - return (typeof cb === 'function' ? cb(null, res.data) : undefined) - }).catch(err => { - let message - console.error('error backing up data: ' + err.message, err) - try { - ({ - message - } = JSON.parse(err.message)) - if (message === 'Not Found') { message = 'Gist ID Not Found' } - } catch (SyntaxError) { - ({ - message - } = err) - } - atom.notifications.addError('sync-settings: Error backing up your settings. (' + message + ')') - return (typeof cb === 'function' ? cb(err) : undefined) - }) - }, - - viewBackup () { - const Shell = require('shell') - const gistId = this.getGistId() - return Shell.openExternal(`https://gist.github.com/${gistId}`) - }, - - getPackages () { - const packages = [] - const object = this._getAvailablePackageMetadataWithoutDuplicates() - for (const i in object) { - const metadata = object[i] - const { name, version, theme, apmInstallSource } = metadata - packages.push({ name, version, theme, apmInstallSource }) - } - return _.sortBy(packages, 'name') - }, - - _getAvailablePackageMetadataWithoutDuplicates () { - let i - const path2metadata = {} - const package_metadata = atom.packages.getAvailablePackageMetadata() - const iterable = atom.packages.getAvailablePackagePaths() - for (i = 0; i < iterable.length; i++) { - const path = iterable[i] - path2metadata[fs.realpathSync(path)] = package_metadata[i] - } - - const packages = [] - const object = atom.packages.getAvailablePackageNames() - for (i in object) { - const pkg_name = object[i] - const pkg_path = atom.packages.resolvePackagePath(pkg_name) - if (path2metadata[pkg_path]) { - packages.push(path2metadata[pkg_path]) - } else { - console.error('could not correlate package name, path, and metadata') - } - } - return packages - }, - - restore (cb = null) { - return this.createClient().gists.get({ - gist_id: this.getGistId() - }).then((res) => { - let file, filename - - // check if the JSON files are parsable - for (filename of Object.keys(res.data.files || {})) { - file = res.data.files[filename] - if ((filename === 'settings.json') || (filename === 'packages.json')) { - try { - JSON.parse(file.content) - } catch (e) { - atom.notifications.addError("sync-settings: Error parsing the fetched JSON file '" + filename + "'. (" + e + ')') - if (typeof cb === 'function') { - cb() - } - return - } - } - } - - let callbackAsync = false - - for (filename of Object.keys(res.data.files || {})) { - file = res.data.files[filename] - switch (filename) { - case 'settings.json': - if (atom.config.get('sync-settings.syncSettings')) { this.applySettings('', JSON.parse(file.content)) } - break - - case 'packages.json': - if (atom.config.get('sync-settings.syncPackages')) { - callbackAsync = true - this.installMissingPackages(JSON.parse(file.content), cb) - if (atom.config.get('sync-settings.removeObsoletePackages')) { - this.removeObsoletePackages(JSON.parse(file.content), cb) - } - } - break - - case 'keymap.cson': - if (atom.config.get('sync-settings.syncKeymap')) { fs.writeFileSync(atom.keymaps.getUserKeymapPath(), file.content) } - break - - case 'styles.less': - if (atom.config.get('sync-settings.syncStyles')) { fs.writeFileSync(atom.styles.getUserStyleSheetPath(), file.content) } - break - - case 'init.coffee': - if (atom.config.get('sync-settings.syncInit')) { fs.writeFileSync(atom.getConfigDirPath() + '/init.coffee', file.content) } - break - - case 'init.js': - if (atom.config.get('sync-settings.syncInit')) { fs.writeFileSync(atom.getConfigDirPath() + '/init.js', file.content) } - break - - case 'snippets.cson': - if (atom.config.get('sync-settings.syncSnippets')) { fs.writeFileSync(atom.getConfigDirPath() + '/snippets.cson', file.content) } - break - - default: fs.writeFileSync(`${atom.getConfigDirPath()}/${filename}`, file.content) - } - } - - atom.config.set('sync-settings._lastBackupHash', res.data.history[0].version) - - atom.notifications.addSuccess('sync-settings: Your settings were successfully synchronized.') - - if (!callbackAsync) { return (typeof cb === 'function' ? cb() : undefined) } - }).catch(err => { - let message - console.error('error while retrieving the gist. does it exists?', err) - try { - ({ - message - } = JSON.parse(err.message)) - if (message === 'Not Found') { message = 'Gist ID Not Found' } - } catch (SyntaxError) { - ({ - message - } = err) - } - atom.notifications.addError('sync-settings: Error retrieving your settings. (' + message + ')') - }) - }, - - createClient () { - const token = this.getPersonalAccessToken() - - if (token) { - console.log(`Creating GitHubApi client with token = ${token.substr(0, 4)}...${token.substr(-4, 4)}`) - } else { - console.log('Creating GitHubApi client without token') - } - - const github = new GitHubApi.Octokit({ - auth: token, - userAgent: 'Atom sync-settings' - }) - - return github - }, - - getFilteredSettings () { - // _.clone() doesn't deep clone thus we are using JSON parse trick - let left - const settings = JSON.parse(JSON.stringify(atom.config.settings)) - const blacklistedKeys = REMOVE_KEYS.concat((left = atom.config.get('sync-settings.blacklistedKeys')) != null ? left : []) - for (let blacklistedKey of Array.from(blacklistedKeys)) { - blacklistedKey = blacklistedKey.split('.') - this._removeProperty(settings, blacklistedKey) - } - return JSON.stringify(settings, null, '\t') - }, - - _removeProperty (obj, key) { - const lastKey = key.length === 1 - const currentKey = key.shift() - - if (!lastKey && _.isObject(obj[currentKey]) && !_.isArray(obj[currentKey])) { - return this._removeProperty(obj[currentKey], key) - } else { - return delete obj[currentKey] - } - }, - - goToPackageSettings () { - return atom.workspace.open('atom://config/packages/sync-settings') - }, - - applySettings (pref, settings) { - return (() => { - const result = [] - for (let key in settings) { - const value = settings[key] - key = key.replace(/\./g, '\\.') - const keyPath = `${pref}.${key}` - let isColor = false - if (_.isObject(value)) { - const valueKeys = Object.keys(value) - const colorKeys = ['alpha', 'blue', 'green', 'red'] - isColor = _.isEqual(_.sortBy(valueKeys), colorKeys) - } - if (_.isObject(value) && !_.isArray(value) && !isColor) { - result.push(this.applySettings(keyPath, value)) - } else { - console.log(`config.set ${keyPath.slice(1)}=${value}`) - result.push(atom.config.set(keyPath.slice(1), value)) - } - } - return result - })() - }, - - removeObsoletePackages (remaining_packages, cb) { - let pkg - const installed_packages = this.getPackages() - const obsolete_packages = [] - for (pkg of Array.from(installed_packages)) { - const keep_installed_package = (Array.from(remaining_packages).filter((p) => p.name === pkg.name)) - if (keep_installed_package.length === 0) { - obsolete_packages.push(pkg) - } - } - if (obsolete_packages.length === 0) { - atom.notifications.addInfo('Sync-settings: no packages to remove') - return (typeof cb === 'function' ? cb() : undefined) - } - - const notifications = {} - const succeeded = [] - const failed = [] - var removeNextPackage = () => { - if (obsolete_packages.length > 0) { - // start removing next package - pkg = obsolete_packages.shift() - const i = succeeded.length + failed.length + Object.keys(notifications).length + 1 - const count = i + obsolete_packages.length - notifications[pkg.name] = atom.notifications.addInfo(`Sync-settings: removing ${pkg.name} (${i}/${count})`, { dismissable: true }) - return (pkg => { - return this.removePackage(pkg, function (error) { - // removal of package finished - notifications[pkg.name].dismiss() - delete notifications[pkg.name] - if (error != null) { - failed.push(pkg.name) - atom.notifications.addWarning(`Sync-settings: failed to remove ${pkg.name}`) - } else { - succeeded.push(pkg.name) - } - // trigger next package - return removeNextPackage() - }) - })(pkg) - } else if (Object.keys(notifications).length === 0) { - // last package removal finished - if (failed.length === 0) { - atom.notifications.addSuccess(`Sync-settings: finished removing ${succeeded.length} packages`) - } else { - failed.sort() - const failedStr = failed.join(', ') - atom.notifications.addWarning(`Sync-settings: finished removing packages (${failed.length} failed: ${failedStr})`, { dismissable: true }) - } - return (typeof cb === 'function' ? cb() : undefined) - } - } - // start as many package removal in parallel as desired - const concurrency = Math.min(obsolete_packages.length, 8) - return (() => { - const result = [] - for (let i = 0, end = concurrency, asc = end >= 0; asc ? i < end : i > end; asc ? i++ : i--) { - result.push(removeNextPackage()) - } - return result - })() - }, - - removePackage (pack, cb) { - const type = pack.theme ? 'theme' : 'package' - console.info(`Removing ${type} ${pack.name}...`) - const packageManager = new PackageManager() - return packageManager.uninstall(pack, function (error) { - if (error != null) { - console.error(`Removing ${type} ${pack.name} failed`, error.stack != null ? error.stack : error, error.stderr) - } else { - console.info(`Removing ${type} ${pack.name}`) - } - return (typeof cb === 'function' ? cb(error) : undefined) - }) - }, - - installMissingPackages (packages, cb) { - let pkg - const available_packages = this.getPackages() - const missing_packages = [] - for (pkg of Array.from(packages)) { - const available_package = (Array.from(available_packages).filter((p) => p.name === pkg.name)) - if (available_package.length === 0) { - // missing if not yet installed - missing_packages.push(pkg) - } else if (!(!!pkg.apmInstallSource === !!available_package[0].apmInstallSource)) { - // or installed but with different apm install source - missing_packages.push(pkg) - } - } - if (missing_packages.length === 0) { - atom.notifications.addInfo('Sync-settings: no packages to install') - return (typeof cb === 'function' ? cb() : undefined) - } - - const notifications = {} - const succeeded = [] - const failed = [] - var installNextPackage = () => { - if (missing_packages.length > 0) { - // start installing next package - pkg = missing_packages.shift() - const i = succeeded.length + failed.length + Object.keys(notifications).length + 1 - const count = i + missing_packages.length - notifications[pkg.name] = atom.notifications.addInfo(`Sync-settings: installing ${pkg.name} (${i}/${count})`, { dismissable: true }) - return (pkg => { - return this.installPackage(pkg, function (error) { - // installation of package finished - notifications[pkg.name].dismiss() - delete notifications[pkg.name] - if (error != null) { - failed.push(pkg.name) - atom.notifications.addWarning(`Sync-settings: failed to install ${pkg.name}`) - } else { - succeeded.push(pkg.name) - } - // trigger next package - return installNextPackage() - }) - })(pkg) - } else if (Object.keys(notifications).length === 0) { - // last package installation finished - if (failed.length === 0) { - atom.notifications.addSuccess(`Sync-settings: finished installing ${succeeded.length} packages`) - } else { - failed.sort() - const failedStr = failed.join(', ') - atom.notifications.addWarning(`Sync-settings: finished installing packages (${failed.length} failed: ${failedStr})`, { dismissable: true }) - } - return (typeof cb === 'function' ? cb() : undefined) - } - } - // start as many package installations in parallel as desired - const concurrency = Math.min(missing_packages.length, 8) - return (() => { - const result = [] - for (let i = 0, end = concurrency, asc = end >= 0; asc ? i < end : i > end; asc ? i++ : i--) { - result.push(installNextPackage()) - } - return result - })() - }, - - installPackage (pack, cb) { - const type = pack.theme ? 'theme' : 'package' - console.info(`Installing ${type} ${pack.name}...`) - const packageManager = new PackageManager() - return packageManager.install(pack, function (error) { - if (error != null) { - console.error(`Installing ${type} ${pack.name} failed`, error.stack != null ? error.stack : error, error.stderr) - } else { - console.info(`Installed ${type} ${pack.name}`) - } - return (typeof cb === 'function' ? cb(error) : undefined) - }) - }, - - fileContent (filePath) { - try { - return fs.readFileSync(filePath, { encoding: 'utf8' }) || null - } catch (e) { - console.error(`Error reading file ${filePath}. Probably doesn't exist.`, e) - return null - } - }, - - inputForkGistId () { - if (ForkGistIdInputView == null) { ForkGistIdInputView = require('./fork-gistid-input-view') } - this.inputView = new ForkGistIdInputView() - return this.inputView.setCallbackInstance(this) - }, - - forkGistId (forkId) { - return this.createClient().gists.fork({ - gist_id: forkId - }).then(res => { - if (res.data.id) { - atom.config.set('sync-settings.gistId', res.data.id) - atom.notifications.addSuccess('sync-settings: Forked successfully to the new Gist ID ' + res.data.id + ' which has been saved to your config.') - } else { - atom.notifications.addError('sync-settings: Error forking settings') - } - }).catch(err => { - let message - try { - ({ - message - } = JSON.parse(err.message)) - if (message === 'Not Found') { message = 'Gist ID Not Found' } - } catch (SyntaxError) { - ({ - message - } = err) - } - atom.notifications.addError('sync-settings: Error forking settings. (' + message + ')') - }) - } -} - -module.exports = SyncSettings - -function __guard__ (value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined +module.exports = { + config: require('./config'), + + activate () { + // speedup activation by async initializing + setImmediate(() => { + // actual initialization after atom has loaded + if (!GitHubApi) { + GitHubApi = require('@octokit/rest') + } + if (!PackageManager) { + PackageManager = require('./package-manager') + } + + const { CompositeDisposable } = require('atom') + this.disposables = new CompositeDisposable() + + this.disposables.add( + atom.commands.add('atom-workspace', 'sync-settings:backup', this.backup.bind(this)), + atom.commands.add('atom-workspace', 'sync-settings:restore', this.restore.bind(this)), + atom.commands.add('atom-workspace', 'sync-settings:view-backup', this.viewBackup.bind(this)), + atom.commands.add('atom-workspace', 'sync-settings:check-backup', this.checkForUpdate.bind(this)), + atom.commands.add('atom-workspace', 'sync-settings:fork', this.inputForkGistId.bind(this)), + ) + + const mandatorySettingsApplied = this.checkMandatorySettings() + if (mandatorySettingsApplied && atom.config.get('sync-settings.checkForUpdatedBackup')) { + this.checkForUpdate() + } + }) + }, + + deactivate () { + this.disposables.dispose() + if (this.inputView) { + this.inputView.destroy() + } + }, + + serialize () {}, + + getGistId () { + let gistId = atom.config.get('sync-settings.gistId') || process.env.GIST_ID + if (gistId) { + gistId = gistId.trim() + } + return gistId + }, + + async getGist () { + return this.createClient().gists.get({ gist_id: this.getGistId() }) + }, + + getPersonalAccessToken () { + let token = atom.config.get('sync-settings.personalAccessToken') || process.env.GITHUB_TOKEN + if (token) { + token = token.trim() + } + return token + }, + + checkMandatorySettings () { + const missingSettings = [] + if (!this.getGistId()) { + missingSettings.push('Gist ID') + } + if (!this.getPersonalAccessToken()) { + missingSettings.push('GitHub personal access token') + } + if (missingSettings.length) { + this.notifyMissingMandatorySettings(missingSettings) + } + return missingSettings.length === 0 + }, + + async checkForUpdate () { + if (!this.getGistId()) { + return this.notifyMissingMandatorySettings(['Gist ID']) + } + + console.debug('checking latest backup...') + try { + const res = await this.getGist() + + if (!res || !res.data || !res.data.history || !res.data.history[0] || !res.data.history[0].version) { + console.error('could not interpret result:', res) + atom.notifications.addError('sync-settings: Error retrieving your settings.') + return + } + + console.debug(`latest backup version ${res.data.history[0].version}`) + if (res.data.history[0].version !== atom.config.get('sync-settings._lastBackupHash')) { + this.notifyNewerBackup() + } else if (!atom.config.get('sync-settings.quietUpdateCheck')) { + this.notifyBackupUptodate() + } + } catch (err) { + console.error('error while retrieving the gist. does it exists?', err) + atom.notifications.addError(`sync-settings: Error retrieving your settings. (${this._gistIdErrorMessage(err)})`) + } + }, + + notifyNewerBackup () { + // we need the actual element for dispatching on it + const workspaceElement = atom.views.getView(atom.workspace) + const notification = atom.notifications.addWarning('sync-settings: Your settings are out of date.', { + dismissable: true, + buttons: [{ + text: 'Backup', + onDidClick () { + atom.commands.dispatch(workspaceElement, 'sync-settings:backup') + notification.dismiss() + }, + }, { + text: 'View backup', + onDidClick () { + atom.commands.dispatch(workspaceElement, 'sync-settings:view-backup') + }, + }, { + text: 'Restore', + onDidClick () { + atom.commands.dispatch(workspaceElement, 'sync-settings:restore') + notification.dismiss() + }, + }, { + text: 'Dismiss', + onDidClick () { + notification.dismiss() + }, + }], + }) + }, + + notifyBackupUptodate () { + atom.notifications.addSuccess('sync-settings: Latest backup is already applied.') + }, + + notifyMissingMandatorySettings (missingSettings) { + const context = this + const errorMsg = 'sync-settings: Mandatory settings missing: ' + missingSettings.join(', ') + + const notification = atom.notifications.addError(errorMsg, { + dismissable: true, + buttons: [{ + text: 'Package settings', + onDidClick () { + context.goToPackageSettings() + notification.dismiss() + }, + }], + }) + }, + + async backup () { + const files = {} + if (atom.config.get('sync-settings.syncSettings')) { + files['settings.json'] = { content: this.getFilteredSettings() } + } + if (atom.config.get('sync-settings.syncPackages')) { + files['packages.json'] = { content: JSON.stringify(this.getPackages(), null, '\t') } + } + if (atom.config.get('sync-settings.syncKeymap')) { + const content = await this.fileContent(atom.keymaps.getUserKeymapPath()) + files['keymap.cson'] = { content: content !== null ? content : '# keymap file (not found)' } + } + if (atom.config.get('sync-settings.syncStyles')) { + const content = await this.fileContent(atom.styles.getUserStyleSheetPath()) + files['styles.less'] = { content: content !== null ? content : '// styles file (not found)' } + } + if (atom.config.get('sync-settings.syncInit')) { + const initPath = atom.getUserInitScriptPath() + const content = await this.fileContent(initPath) + const path = require('path') + files[path.basename(initPath)] = { content: content !== null ? content : '# initialization file (not found)' } + } + if (atom.config.get('sync-settings.syncSnippets')) { + const content = await this.fileContent(atom.getConfigDirPath() + '/snippets.cson') + files['snippets.cson'] = { content: content !== null ? content : '# snippets file (not found)' } + } + + const extraFiles = atom.config.get('sync-settings.extraFiles') || [] + for (const file of extraFiles) { + const ext = file.slice(file.lastIndexOf('.')).toLowerCase() + let cmtstart = '#' + let cmtend = '' + if (['.less', '.scss', '.js'].includes(ext)) { + cmtstart = '//' + } + if (['.css'].includes(ext)) { + cmtstart = '/*' + } + if (['.css'].includes(ext)) { + cmtend = '*/' + } + const content = await this.fileContent(atom.getConfigDirPath() + `/${file}`) + files[file] = { content: content !== null ? content : `${cmtstart} ${file} (not found) ${cmtend}` } + } + + try { + const res = await this.createClient().gists.update({ + gist_id: this.getGistId(), + description: atom.config.get('sync-settings.gistDescription'), + files, + }) + + atom.config.set('sync-settings._lastBackupHash', res.data.history[0].version) + atom.notifications.addSuccess(`sync-settings: Your settings were successfully backed up.
Click here to open your Gist.`) + } catch (err) { + console.error('error backing up data: ' + err.message, err) + atom.notifications.addError(`sync-settings: Error backing up your settings. (${this._gistIdErrorMessage(err)})`) + } + }, + + viewBackup () { + const Shell = require('shell') + const gistId = this.getGistId() + Shell.openExternal(`https://gist.github.com/${gistId}`) + }, + + getPackages () { + const packages = [] + const object = this._getAvailablePackageMetadataWithoutDuplicates() + for (const i in object) { + const metadata = object[i] + const { name, version, theme, apmInstallSource } = metadata + packages.push({ name, version, theme, apmInstallSource }) + } + return _.sortBy(packages, 'name') + }, + + _getAvailablePackageMetadataWithoutDuplicates () { + const path2metadata = {} + const packageMetadata = atom.packages.getAvailablePackageMetadata() + const iterable = atom.packages.getAvailablePackagePaths() + for (let i = 0; i < iterable.length; i++) { + const path = iterable[i] + path2metadata[fs.realpathSync(path)] = packageMetadata[i] + } + + const packages = [] + const object = atom.packages.getAvailablePackageNames() + for (const prop in object) { + const pkgName = object[prop] + const pkgPath = atom.packages.resolvePackagePath(pkgName) + if (path2metadata[pkgPath]) { + packages.push(path2metadata[pkgPath]) + } else { + console.error('could not correlate package name, path, and metadata') + } + } + return packages + }, + + async restore () { + try { + const res = await this.getGist() + const files = Object.keys(res.data.files) + + // check if the JSON files are parsable + for (const filename of files) { + const file = res.data.files[filename] + if (filename === 'settings.json' || filename === 'packages.json') { + try { + JSON.parse(file.content) + } catch (err) { + atom.notifications.addError(`sync-settings: Error parsing the fetched JSON file '${filename}'. (${err})`) + return + } + } + } + + const configDirPath = atom.getConfigDirPath() + for (const filename of files) { + const file = res.data.files[filename] + switch (filename) { + case 'settings.json': + if (atom.config.get('sync-settings.syncSettings')) { + this.applySettings('', JSON.parse(file.content)) + } + break + + case 'packages.json': { + if (atom.config.get('sync-settings.syncPackages')) { + const packages = JSON.parse(file.content) + await this.installMissingPackages(packages) + if (atom.config.get('sync-settings.removeObsoletePackages')) { + await this.removeObsoletePackages(packages) + } + } + break + } + + case 'keymap.cson': + if (atom.config.get('sync-settings.syncKeymap')) { + await writeFile(atom.keymaps.getUserKeymapPath(), file.content) + } + break + + case 'styles.less': + if (atom.config.get('sync-settings.syncStyles')) { + await writeFile(atom.styles.getUserStyleSheetPath(), file.content) + } + break + + case 'init.coffee': + if (atom.config.get('sync-settings.syncInit')) { + await writeFile(configDirPath + '/init.coffee', file.content) + } + break + + case 'init.js': + if (atom.config.get('sync-settings.syncInit')) { + await writeFile(configDirPath + '/init.js', file.content) + } + break + + case 'snippets.cson': + if (atom.config.get('sync-settings.syncSnippets')) { + await writeFile(configDirPath + '/snippets.cson', file.content) + } + break + + default: + await writeFile(`${configDirPath}/${filename}`, file.content) + } + } + + atom.config.set('sync-settings._lastBackupHash', res.data.history[0].version) + + atom.notifications.addSuccess('sync-settings: Your settings were successfully synchronized.') + } catch (err) { + console.error('error while retrieving the gist. does it exists?', err) + atom.notifications.addError(`sync-settings: Error retrieving your settings. (${this._gistIdErrorMessage(err)})`) + throw err + } + }, + + createClient () { + const token = this.getPersonalAccessToken() + + if (token) { + console.debug(`Creating GitHubApi client with token = ${token.substr(0, 4)}...${token.substr(-4, 4)}`) + } else { + console.error('Creating GitHubApi client without token') + } + + const github = new GitHubApi.Octokit({ + auth: token, + userAgent: 'Atom sync-settings', + }) + + return github + }, + + getFilteredSettings () { + // _.clone() doesn't deep clone thus we are using JSON parse trick + const settings = JSON.parse(JSON.stringify(atom.config.settings)) + const blacklistedKeys = [ + ...REMOVE_KEYS, + ...atom.config.get('sync-settings.blacklistedKeys') || [], + ] + for (let blacklistedKey of blacklistedKeys) { + blacklistedKey = blacklistedKey.split('.') + this._removeProperty(settings, blacklistedKey) + } + return JSON.stringify(settings, null, '\t') + }, + + _removeProperty (obj, key) { + const lastKey = key.length === 1 + const currentKey = key.shift() + + if (!lastKey && _.isObject(obj[currentKey]) && !_.isArray(obj[currentKey])) { + this._removeProperty(obj[currentKey], key) + } else { + delete obj[currentKey] + } + }, + + goToPackageSettings () { + return atom.workspace.open('atom://config/packages/sync-settings') + }, + + applySettings (pref, settings) { + for (let key in settings) { + const value = settings[key] + key = key.replace(/\./g, '\\.') + const keyPath = `${pref}.${key}` + let isColor = false + if (_.isObject(value)) { + const valueKeys = Object.keys(value) + const colorKeys = ['alpha', 'blue', 'green', 'red'] + isColor = _.isEqual(_.sortBy(valueKeys), colorKeys) + } + if (_.isObject(value) && !_.isArray(value) && !isColor) { + this.applySettings(keyPath, value) + } else { + console.debug(`config.set ${keyPath.slice(1)}=${value}`) + atom.config.set(keyPath.slice(1), value) + } + } + }, + + async removeObsoletePackages (packages) { + const installedPackages = this.getPackages().map(p => p.name) + const removePackages = packages.filter(p => !installedPackages.includes(p.name)) + if (removePackages.length === 0) { + atom.notifications.addInfo('Sync-settings: no packages to remove') + return + } + + const total = removePackages.length + const notifications = {} + const succeeded = [] + const failed = [] + const removeNextPackage = async () => { + if (removePackages.length > 0) { + // start removing next package + const pkg = removePackages.shift() + const i = total - removePackages.length + notifications[pkg.name] = atom.notifications.addInfo(`Sync-settings: removing ${pkg.name} (${i}/${total})`, { dismissable: true }) + + try { + await this.removePackage(pkg) + succeeded.push(pkg.name) + } catch (err) { + failed.push(pkg.name) + atom.notifications.addWarning(`Sync-settings: failed to remove ${pkg.name}`) + } + + notifications[pkg.name].dismiss() + delete notifications[pkg.name] + + return removeNextPackage() + } else if (Object.keys(notifications).length === 0) { + // last package removed + if (failed.length === 0) { + atom.notifications.addSuccess(`Sync-settings: finished removing ${succeeded.length} packages`) + } else { + failed.sort() + const failedStr = failed.join(', ') + atom.notifications.addWarning(`Sync-settings: finished removing packages (${failed.length} failed: ${failedStr})`, { dismissable: true }) + } + } + } + // start as many package removal in parallel as desired + const concurrency = Math.min(removePackages.length, 8) + const result = [] + for (let i = 0; i < concurrency; i++) { + result.push(removeNextPackage()) + } + await Promise.all(result) + }, + + async removePackage (pkg) { + const type = pkg.theme ? 'theme' : 'package' + console.info(`Removing ${type} ${pkg.name}...`) + await new Promise((resolve, reject) => { + // TODO: should packageManager be cached? + const packageManager = new PackageManager() + packageManager.uninstall(pkg, (err) => { + if (err) { + console.error( + `Removing ${type} ${pkg.name} failed`, + err.stack ? err.stack : err, + err.stderr, + ) + reject(err) + } else { + console.info(`Removing ${type} ${pkg.name}`) + resolve() + } + }) + }) + }, + + async installMissingPackages (packages) { + const availablePackages = this.getPackages() + const missingPackages = packages.filter(p => { + const availablePackage = availablePackages.find(ap => ap.name === p.name) + return !availablePackage || !!p.apmInstallSource !== !!availablePackage.apmInstallSource + }) + if (missingPackages.length === 0) { + atom.notifications.addInfo('Sync-settings: no packages to install') + return + } + + const total = missingPackages.length + const notifications = {} + const succeeded = [] + const failed = [] + const installNextPackage = async () => { + if (missingPackages.length > 0) { + // start installing next package + const pkg = missingPackages.shift() + const i = total - missingPackages.length + notifications[pkg.name] = atom.notifications.addInfo(`Sync-settings: installing ${pkg.name} (${i}/${total})`, { dismissable: true }) + + try { + await this.installPackage(pkg) + succeeded.push(pkg.name) + } catch (err) { + failed.push(pkg.name) + atom.notifications.addWarning(`Sync-settings: failed to install ${pkg.name}`) + } + + notifications[pkg.name].dismiss() + delete notifications[pkg.name] + + return installNextPackage() + } else if (Object.keys(notifications).length === 0) { + // last package installation finished + if (failed.length === 0) { + atom.notifications.addSuccess(`Sync-settings: finished installing ${succeeded.length} packages`) + } else { + failed.sort() + const failedStr = failed.join(', ') + atom.notifications.addWarning(`Sync-settings: finished installing packages (${failed.length} failed: ${failedStr})`, { dismissable: true }) + } + } + } + // start as many package installations in parallel as desired + const concurrency = Math.min(missingPackages.length, 8) + const result = [] + for (let i = 0; i < concurrency; i++) { + result.push(installNextPackage()) + } + await Promise.all(result) + }, + + async installPackage (pkg) { + const type = pkg.theme ? 'theme' : 'package' + console.info(`Installing ${type} ${pkg.name}...`) + await new Promise((resolve, reject) => { + // TODO: should packageManager be cached? + const packageManager = new PackageManager() + packageManager.install(pkg, (err) => { + if (err) { + console.error( + `Installing ${type} ${pkg.name} failed`, + err.stack ? err.stack : err, + err.stderr, + ) + reject(err) + } else { + console.info(`Installed ${type} ${pkg.name}`) + resolve() + } + }) + }) + }, + + async fileContent (filePath) { + try { + const content = await readFile(filePath, { encoding: 'utf8' }) + return content !== '' ? content : null + } catch (err) { + console.error(`Error reading file ${filePath}. Probably doesn't exist.`, err) + return null + } + }, + + inputForkGistId () { + if (!ForkGistIdInputView) { + ForkGistIdInputView = require('./fork-gistid-input-view') + } + this.inputView = new ForkGistIdInputView(this) + }, + + async forkGistId (forkId) { + try { + const res = await this.createClient().gists.fork({ gist_id: forkId }) + if (res.data.id) { + atom.config.set('sync-settings.gistId', res.data.id) + atom.notifications.addSuccess(`sync-settings: Forked successfully to the new Gist ID ${res.data.id} which has been saved to your config.`) + } else { + atom.notifications.addError('sync-settings: Error forking settings') + } + } catch (err) { + atom.notifications.addError(`sync-settings: Error forking settings. (${this._gistIdErrorMessage(err)})`) + } + }, + + _gistIdErrorMessage (err) { + let message + try { + message = JSON.parse(err.message).message + if (message === 'Not Found') { + message = 'Gist ID Not Found' + } + } catch (SyntaxError) { + message = err.message + } + return message + }, } diff --git a/package-lock.json b/package-lock.json index 64577e37..cd1afcf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -887,11 +887,45 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, "atob-lite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=" }, + "atom-jasmine3-test-runner": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/atom-jasmine3-test-runner/-/atom-jasmine3-test-runner-4.3.12.tgz", + "integrity": "sha512-r2CUAbVijYahBba86zcc7lGAvouoVbyeerZRQNKPrwGM0KfdFf6jSYRztuIxAAyvkbWa44qTzl2jeI1lw4CPfQ==", + "dev": true, + "requires": { + "etch": "^0.14.0", + "find-parent-dir": "^0.3.0", + "fs-plus": "3.1.1", + "glob": "^7.1.6", + "grim": "^2.0.2", + "jasmine": "~3.5.0", + "semver": "^7.1.2", + "temp": "^0.9.1", + "underscore-plus": "^1.7.0" + }, + "dependencies": { + "grim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/grim/-/grim-2.0.2.tgz", + "integrity": "sha512-Qj7hTJRfd87E/gUgfvM0YIH/g2UA2SV6niv6BYXk1o6w4mhgv+QyYM1EjOJQljvzgEj4SqSsRWldXIeKHz3e3Q==", + "dev": true, + "requires": { + "event-kit": "^2.0.0" + } + } + } + }, "atom-space-pen-views": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/atom-space-pen-views/-/atom-space-pen-views-2.2.0.tgz", @@ -1887,6 +1921,18 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etch": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/etch/-/etch-0.14.0.tgz", + "integrity": "sha512-puqbFxz7lSm+YK6Q+bvRkNndRv6PRvGscSEhcFjmtL4nX/Az5rRCNPvK3aVTde85c/L5X0vI5kqfnpYddRalJQ==", + "dev": true + }, + "event-kit": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/event-kit/-/event-kit-2.5.3.tgz", + "integrity": "sha512-b7Qi1JNzY4BfAYfnIRanLk0DOD1gdkWHT4GISIn8Q2tAf3LpU8SP2CMwWaq40imYoKWbtN4ZhbSRxvsnikooZQ==", + "dev": true + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -1994,6 +2040,12 @@ "to-regex-range": "^5.0.1" } }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -2050,6 +2102,18 @@ "universalify": "^0.1.0" } }, + "fs-plus": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz", + "integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==", + "dev": true, + "requires": { + "async": "^1.5.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2", + "underscore-plus": "1.x" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2514,6 +2578,22 @@ "lodash.uniqby": "^4.7.0" } }, + "jasmine": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", + "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "jasmine-core": "~3.5.0" + } + }, + "jasmine-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", + "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "dev": true + }, "java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -7692,6 +7772,15 @@ } } }, + "temp": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", + "integrity": "sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA==", + "dev": true, + "requires": { + "rimraf": "~2.6.2" + } + }, "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", diff --git a/package.json b/package.json index cdae477c..1759ad4d 100644 --- a/package.json +++ b/package.json @@ -28,14 +28,15 @@ "scripts": { "lint": "eslint ." }, + "atomTestRunner": "atom-jasmine3-test-runner", "dependencies": { "@octokit/rest": "^16.43.1", - "atom-space-pen-views": "^2.2.0", "semver": "^7.1.2", "underscore-plus": "^1.7.0" }, "devDependencies": { "@semantic-release/apm-config": "^8.0.0", + "atom-jasmine3-test-runner": "^4.3.12", "eslint": "^6.8.0", "eslint-config-standard": "^14.1.0", "eslint-plugin-import": "^2.20.1", diff --git a/release.config.js b/release.config.js index 2c4c8c2b..df047469 100644 --- a/release.config.js +++ b/release.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: '@semantic-release/apm-config' + extends: '@semantic-release/apm-config', } diff --git a/spec/create-client-mock.js b/spec/create-client-mock.js new file mode 100644 index 00000000..56bb4d43 --- /dev/null +++ b/spec/create-client-mock.js @@ -0,0 +1,102 @@ +function mergeFiles (gistFiles, files) { + for (const filename in files) { + const file = files[filename] + if (file.filename === null) { + delete gistFiles[filename] + } else if (filename in gistFiles) { + gistFiles[filename].content = file.content + } else { + gistFiles[filename] = { + content: file.content, + filename, + } + } + } + + return gistFiles +} + +function randomString (len = 5) { + let str = '' + while (str.length < len) { + str += Math.random().toString(36).substr(2) + } + return str.substring(0, len) +} + +module.exports = class Client { + constructor () { + this.gistCache = {} + } + + get gists () { + const self = this + return { + async get ({ gist_id: gistId }) { + console.debug('GITHUB_TOKEN does not exist. Mocking API calls.') + + if (!(gistId in self.gistCache)) { + throw new Error(JSON.stringify({ message: 'Not Found' })) + } + + return { + data: self.gistCache[gistId], + } + }, + + async update ({ gist_id: gistId, description, files }) { + console.debug('GITHUB_TOKEN does not exist. Mocking API calls.') + + if (!(gistId in self.gistCache)) { + throw new Error(JSON.stringify({ message: 'Not Found' })) + } + + const gist = self.gistCache[gistId] + gist.description = description + gist.files = mergeFiles(gist.files, files) + gist.history.unshift({ version: `${gist.id}-${randomString()}` }) + + return { + data: gist, + } + }, + + async fork ({ gist_id: gistId }) { + console.debug('GITHUB_TOKEN does not exist. Mocking API calls.') + + if (!(gistId in self.gistCache)) { + throw new Error(JSON.stringify({ message: 'Not Found' })) + } + + return this.create({ + description: self.gistCache[gistId].description, + files: self.gistCache[gistId].files, + }) + }, + + async create ({ description, files }) { + console.debug('GITHUB_TOKEN does not exist. Mocking API calls.') + + const gistId = `mock-${randomString()}` + const gist = { + id: gistId, + description, + files: mergeFiles({}, files), + history: [{ version: `${gistId}-${randomString()}` }], + html_url: `https://${gistId}`, + } + self.gistCache[gistId] = gist + + return { + data: gist, + } + }, + + async delete ({ gist_id: gistId }) { + console.debug('GITHUB_TOKEN does not exist. Mocking API calls.') + + delete self.gistCache[gistId] + }, + } + } +} diff --git a/spec/spec-helpers.js b/spec/spec-helpers.js deleted file mode 100644 index 1403fa28..00000000 --- a/spec/spec-helpers.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -module.exports = { - setConfig (keyPath, value) { - if (this.originalConfigs == null) { this.originalConfigs = {} } - if (this.originalConfigs[keyPath] == null) { this.originalConfigs[keyPath] = atom.config.isDefault(keyPath) ? null : atom.config.get(keyPath) } - return atom.config.set(keyPath, value) - }, - - restoreConfigs () { - if (this.originalConfigs) { - return (() => { - const result = [] - for (const keyPath in this.originalConfigs) { - const value = this.originalConfigs[keyPath] - result.push(atom.config.set(keyPath, value)) - } - return result - })() - } - }, - - callAsync (timeout, async, next) { - if (typeof timeout === 'function') { - [async, next] = [timeout, async] - timeout = 5000 - } - let done = false - let nextArgs = null - - runs(() => async(function (...args) { - done = true - nextArgs = args - })) - - waitsFor(() => done - , null, timeout) - - if (next != null) { - return runs(function () { - return next.apply(this, nextArgs) - }) - } - } -} diff --git a/spec/sync-settings-spec.js b/spec/sync-settings-spec.js index f6da0e4c..30c0768c 100644 --- a/spec/sync-settings-spec.js +++ b/spec/sync-settings-spec.js @@ -1,16 +1,9 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const SyncSettings = require('../lib/sync-settings') -const SpecHelper = require('./spec-helpers') -const run = SpecHelper.callAsync +const CreateClient = require('./create-client-mock') const fs = require('fs') +const util = require('util') +const writeFile = util.promisify(fs.writeFile) +const unlink = util.promisify(fs.unlink) const path = require('path') const os = require('os') // Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. @@ -18,340 +11,313 @@ const os = require('os') // To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` // or `fdescribe`). Remove the `f` to unfocus the block. -describe('SyncSettings', function () { - describe('low-level', () => describe('::fileContent', function () { - const tmpPath = path.join(os.tmpdir(), 'atom-sync-settings.tmp') - - it('returns null for not existing file', () => expect(SyncSettings.fileContent(tmpPath)).toBeNull()) - - it('returns null for empty file', function () { - fs.writeFileSync(tmpPath, '') - try { - return expect(SyncSettings.fileContent(tmpPath)).toBeNull() - } finally { - fs.unlinkSync(tmpPath) - } - }) - - return it('returns content of existing file', function () { - const text = 'alabala portocala' - fs.writeFileSync(tmpPath, text) - try { - return expect(SyncSettings.fileContent(tmpPath)).toEqual(text) - } finally { - fs.unlinkSync(tmpPath) - } - }) - })) - - return describe('high-level', function () { - const TOKEN_CONFIG = 'sync-settings.personalAccessToken' - const GIST_ID_CONFIG = 'sync-settings.gistId' - - window.resetTimeouts() - SyncSettings.activate() - window.advanceClock() - - beforeEach(function () { - this.token = process.env.GITHUB_TOKEN || atom.config.get(TOKEN_CONFIG) - atom.config.set(TOKEN_CONFIG, this.token) - - return run(function (cb) { - const gistSettings = { - public: false, - description: 'Test gist by Sync Settings for Atom https://github.com/atom-community/sync-settings', - files: { README: { content: '# Generated by Sync Settings for Atom https://github.com/atom-community/sync-settings' } } - } - return SyncSettings.createClient().gists.create(gistSettings).then(a => cb(null, a), cb) - } - , (err, res) => { - expect(err).toBeNull() - - this.gistId = res.data.id - console.log(`Using Gist ${this.gistId}`) - return atom.config.set(GIST_ID_CONFIG, this.gistId) - }) - }) - - afterEach(function () { - return run(cb => { - return SyncSettings.createClient().gists.delete({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(err).toBeNull()) - }) - - describe('::backup', function () { - it('back up the settings', function () { - atom.config.set('sync-settings.syncSettings', true) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['settings.json']).toBeDefined()) - }) - }) - - it("don't back up the settings", function () { - atom.config.set('sync-settings.syncSettings', false) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['settings.json']).not.toBeDefined()) - }) - }) - - it('back up the installed packages list', function () { - atom.config.set('sync-settings.syncPackages', true) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['packages.json']).toBeDefined()) - }) - }) - - it("don't back up the installed packages list", function () { - atom.config.set('sync-settings.syncPackages', false) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['packages.json']).not.toBeDefined()) - }) - }) - - it('back up the user keymaps', function () { - atom.config.set('sync-settings.syncKeymap', true) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['keymap.cson']).toBeDefined()) - }) - }) - - it("don't back up the user keymaps", function () { - atom.config.set('sync-settings.syncKeymap', false) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['keymap.cson']).not.toBeDefined()) - }) - }) - - it('back up the user styles', function () { - atom.config.set('sync-settings.syncStyles', true) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['styles.less']).toBeDefined()) - }) - }) - - it("don't back up the user styles", function () { - atom.config.set('sync-settings.syncStyles', false) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['styles.less']).not.toBeDefined()) - }) - }) - - it('back up the user init script file', function () { - atom.config.set('sync-settings.syncInit', true) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files[path.basename(atom.getUserInitScriptPath())]).toBeDefined()) - }) - }) - - it("don't back up the user init script file", function () { - atom.config.set('sync-settings.syncInit', false) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files[path.basename(atom.getUserInitScriptPath())]).not.toBeDefined()) - }) - }) - - it('back up the user snippets', function () { - atom.config.set('sync-settings.syncSnippets', true) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['snippets.cson']).toBeDefined()) - }) - }) - - it("don't back up the user snippets", function () { - atom.config.set('sync-settings.syncSnippets', false) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(res.data.files['snippets.cson']).not.toBeDefined()) - }) - }) - - it('back up the files defined in config.extraFiles', function () { - atom.config.set('sync-settings.extraFiles', ['test.tmp', 'test2.tmp']) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => Array.from(atom.config.get('sync-settings.extraFiles')).map((file) => - expect(res.data.files[file]).toBeDefined())) - }) - }) - - return it("don't back up extra files defined in config.extraFiles", function () { - atom.config.set('sync-settings.extraFiles', undefined) - return run(cb => SyncSettings.backup(cb) - , function () { - return run(cb => { - return SyncSettings.createClient().gists.get({ gist_id: this.gistId }).then(a => cb(null, a), cb) - } - , (err, res) => expect(Object.keys(res.data.files).length).toBe(1)) - }) - }) - }) - - describe('::restore', function () { - it('updates settings', function () { - atom.config.set('sync-settings.syncSettings', true) - atom.config.set('some-dummy', true) - return run(cb => SyncSettings.backup(cb) - , function () { - atom.config.set('some-dummy', false) - return run(cb => SyncSettings.restore(cb) - , () => expect(atom.config.get('some-dummy')).toBeTruthy()) - }) - }) - - it("doesn't updates settings", function () { - atom.config.set('sync-settings.syncSettings', false) - atom.config.set('some-dummy', true) - return run(cb => SyncSettings.backup(cb) - , () => run(cb => SyncSettings.restore(cb) - , () => expect(atom.config.get('some-dummy')).toBeTruthy())) - }) - - it('overrides keymap.cson', function () { - let left - atom.config.set('sync-settings.syncKeymap', true) - const original = (left = SyncSettings.fileContent(atom.keymaps.getUserKeymapPath())) != null ? left : '# keymap file (not found)' - return run(cb => SyncSettings.backup(cb) - , function () { - fs.writeFileSync(atom.keymaps.getUserKeymapPath(), `${original}\n# modified by sync setting spec`) - return run(cb => SyncSettings.restore(cb) - , function () { - expect(SyncSettings.fileContent(atom.keymaps.getUserKeymapPath())).toEqual(original) - return fs.writeFileSync(atom.keymaps.getUserKeymapPath(), original) - }) - }) - }) - - it('restores all other files in the gist as well', function () { - atom.config.set('sync-settings.extraFiles', ['test.tmp', 'test2.tmp']) - return run(cb => SyncSettings.backup(cb) - , () => run(cb => SyncSettings.restore(cb) - , () => (() => { - const result = [] - for (const file of Array.from(atom.config.get('sync-settings.extraFiles'))) { - expect(fs.existsSync(`${atom.getConfigDirPath()}/${file}`)).toBe(true) - expect(SyncSettings.fileContent(`${atom.getConfigDirPath()}/${file}`)).toBe(`# ${file} (not found) `) - result.push(fs.unlink(`${atom.getConfigDirPath()}/${file}`, function () {})) - } - return result - })())) - }) - - it('skips the restore due to invalid json', function () { - atom.config.set('sync-settings.syncSettings', true) - atom.config.set('sync-settings.extraFiles', ['packages.json']) - atom.config.set('some-dummy', false) - return run(cb => SyncSettings.backup(cb) - , function () { - atom.config.set('some-dummy', true) - atom.notifications.clear() - - return run(cb => SyncSettings.restore(cb) - , function () { - expect(atom.notifications.getNotifications().length).toEqual(1) - expect(atom.notifications.getNotifications()[0].getType()).toBe('error') - // the value should not be restored - // since the restore valid to parse the input as valid json - return expect(atom.config.get('some-dummy')).toBeTruthy() - }) - }) - }) - - return it('restores keys with dots', function () { - atom.config.set('sync-settings.syncSettings', true) - atom.config.set('some\\.key', ['one', 'two']) - return run(cb => SyncSettings.backup(cb) - , function () { - atom.config.set('some\\.key', ['two']) - - return run(cb => SyncSettings.restore(cb) - , function () { - expect(atom.config.get('some\\.key').length).toBe(2) - expect(atom.config.get('some\\.key')[0]).toBe('one') - return expect(atom.config.get('some\\.key')[1]).toBe('two') - }) - }) - }) - }) - - return describe('::check for update', function () { - beforeEach(() => atom.config.unset('sync-settings._lastBackupHash')) - - it('updates last hash on backup', () => run(cb => SyncSettings.backup(cb) - , () => expect(atom.config.get('sync-settings._lastBackupHash')).toBeDefined())) - - it('updates last hash on restore', () => run(cb => SyncSettings.restore(cb) - , () => expect(atom.config.get('sync-settings._lastBackupHash')).toBeDefined())) - - return describe('::notification', function () { - beforeEach(() => atom.notifications.clear()) - - it('displays on newer backup', () => run(cb => SyncSettings.checkForUpdate(cb) - , function () { - expect(atom.notifications.getNotifications().length).toBe(1) - return expect(atom.notifications.getNotifications()[0].getType()).toBe('warning') - })) - - return it('ignores on up-to-date backup', () => run(cb => SyncSettings.backup(cb) - , () => run(function (cb) { - atom.notifications.clear() - return SyncSettings.checkForUpdate(cb) - } - , function () { - expect(atom.notifications.getNotifications().length).toBe(1) - return expect(atom.notifications.getNotifications()[0].getType()).toBe('success') - }))) - }) - }) - }) +describe('SyncSettings', () => { + describe('low-level', () => { + it('should activate and destroy without error', async () => { + await atom.packages.activatePackage('sync-settings') + // wait for package to activate + await new Promise(resolve => setImmediate(resolve)) + await atom.packages.deactivatePackage('sync-settings') + }) + + describe('::fileContent', () => { + const tmpPath = path.join(os.tmpdir(), 'atom-sync-settings.tmp') + + it('returns null for not existing file', async () => { + expect(await SyncSettings.fileContent(tmpPath)).toBeNull() + }) + + it('returns null for empty file', async () => { + await writeFile(tmpPath, '') + try { + expect(await SyncSettings.fileContent(tmpPath)).toBeNull() + } finally { + await unlink(tmpPath) + } + }) + + it('returns content of existing file', async () => { + const text = 'alabala portocala' + await writeFile(tmpPath, text) + try { + expect(await SyncSettings.fileContent(tmpPath)).toEqual(text) + } finally { + await unlink(tmpPath) + } + }) + }) + }) + + describe('high-level', () => { + beforeEach(async () => { + await atom.packages.activatePackage('sync-settings') + // wait for package to activate + await new Promise(resolve => setImmediate(resolve)) + + if (!process.env.GITHUB_TOKEN) { + console.error('GITHUB_TOKEN does not exist. Mocking API calls.') + spyOn(SyncSettings, 'createClient').and.returnValue(new CreateClient()) + } + + const token = process.env.GITHUB_TOKEN || atom.config.get('sync-settings.personalAccessToken') + atom.config.set('sync-settings.personalAccessToken', token) + + const gistSettings = { + public: false, + description: 'Test gist by Sync Settings for Atom https://github.com/atom-community/sync-settings', + files: { README: { content: '# Generated by Sync Settings for Atom https://github.com/atom-community/sync-settings' } }, + } + + const res = await SyncSettings.createClient().gists.create(gistSettings) + + console.log(`Using Gist ${res.data.id}`) + atom.config.set('sync-settings.gistId', res.data.id) + }) + + afterEach(async () => { + await SyncSettings.createClient().gists.delete({ gist_id: SyncSettings.getGistId() }) + await await atom.packages.deactivatePackage('sync-settings') + }) + + describe('::backup', () => { + it('back up the settings', async () => { + atom.config.set('sync-settings.syncSettings', true) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['settings.json']).toBeDefined() + }) + + it("don't back up the settings", async () => { + atom.config.set('sync-settings.syncSettings', false) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['settings.json']).not.toBeDefined() + }) + + it('back up the installed packages list', async () => { + atom.config.set('sync-settings.syncPackages', true) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['packages.json']).toBeDefined() + }) + + it("don't back up the installed packages list", async () => { + atom.config.set('sync-settings.syncPackages', false) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['packages.json']).not.toBeDefined() + }) + + it('back up the user keymaps', async () => { + atom.config.set('sync-settings.syncKeymap', true) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['keymap.cson']).toBeDefined() + }) + + it("don't back up the user keymaps", async () => { + atom.config.set('sync-settings.syncKeymap', false) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['keymap.cson']).not.toBeDefined() + }) + + it('back up the user styles', async () => { + atom.config.set('sync-settings.syncStyles', true) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['styles.less']).toBeDefined() + }) + + it("don't back up the user styles", async () => { + atom.config.set('sync-settings.syncStyles', false) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['styles.less']).not.toBeDefined() + }) + + it('back up the user init script file', async () => { + atom.config.set('sync-settings.syncInit', true) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files[path.basename(atom.getUserInitScriptPath())]).toBeDefined() + }) + + it("don't back up the user init script file", async () => { + atom.config.set('sync-settings.syncInit', false) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files[path.basename(atom.getUserInitScriptPath())]).not.toBeDefined() + }) + + it('back up the user snippets', async () => { + atom.config.set('sync-settings.syncSnippets', true) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['snippets.cson']).toBeDefined() + }) + + it("don't back up the user snippets", async () => { + atom.config.set('sync-settings.syncSnippets', false) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(res.data.files['snippets.cson']).not.toBeDefined() + }) + + it('back up the files defined in config.extraFiles', async () => { + atom.config.set('sync-settings.extraFiles', ['test.tmp', 'test2.tmp']) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + atom.config.get('sync-settings.extraFiles').forEach(file => { + expect(res.data.files[file]).toBeDefined() + }) + }) + + it("don't back up extra files defined in config.extraFiles", async () => { + atom.config.set('sync-settings.extraFiles', undefined) + await SyncSettings.backup() + const res = await SyncSettings.getGist() + + expect(Object.keys(res.data.files).length).toBe(7) + }) + }) + + describe('::restore', () => { + it('updates settings', async () => { + atom.config.set('sync-settings.syncSettings', true) + atom.config.set('some-dummy', true) + await SyncSettings.backup() + atom.config.set('some-dummy', false) + await SyncSettings.restore() + + expect(atom.config.get('some-dummy')).toBeTruthy() + }) + + it("doesn't updates settings", async () => { + atom.config.set('sync-settings.syncSettings', false) + atom.config.set('some-dummy', true) + await SyncSettings.backup() + await SyncSettings.restore() + + expect(atom.config.get('some-dummy')).toBeTruthy() + }) + + it('overrides keymap.cson', async () => { + atom.config.set('sync-settings.syncKeymap', true) + let original = await SyncSettings.fileContent(atom.keymaps.getUserKeymapPath()) + if (!original) { + original = '# keymap file (not found)' + } + + try { + await SyncSettings.backup() + await writeFile(atom.keymaps.getUserKeymapPath(), `${original}\n# modified by sync setting spec`) + await SyncSettings.restore() + const content = await SyncSettings.fileContent(atom.keymaps.getUserKeymapPath()) + + expect(content).toEqual(original) + } finally { + await writeFile(atom.keymaps.getUserKeymapPath(), original) + } + }) + + it('restores all other files in the gist as well', async () => { + const files = ['test.tmp', 'test2.tmp'] + atom.config.set('sync-settings.extraFiles', files) + try { + await SyncSettings.backup() + await SyncSettings.restore() + + for (const file of files) { + expect(fs.existsSync(`${atom.getConfigDirPath()}/${file}`)).toBe(true) + expect(await SyncSettings.fileContent(`${atom.getConfigDirPath()}/${file}`)).toBe(`# ${file} (not found) `) + } + } finally { + for (const file of files) { + await unlink(`${atom.getConfigDirPath()}/${file}`) + } + } + }) + + it('skips the restore due to invalid json', async () => { + atom.config.set('sync-settings.syncSettings', true) + atom.config.set('sync-settings.extraFiles', ['packages.json']) + atom.config.set('some-dummy', false) + await SyncSettings.backup() + atom.config.set('some-dummy', true) + atom.notifications.clear() + await SyncSettings.restore() + + expect(atom.notifications.getNotifications().length).toEqual(1) + expect(atom.notifications.getNotifications()[0].getType()).toBe('error') + // the value should not be restored + // since the restore valid to parse the input as valid json + expect(atom.config.get('some-dummy')).toBeTruthy() + }) + + it('restores keys with dots', async () => { + atom.config.set('sync-settings.syncSettings', true) + atom.config.set('some\\.key', ['one', 'two']) + await SyncSettings.backup() + atom.config.set('some\\.key', ['two']) + await SyncSettings.restore() + + expect(atom.config.get('some\\.key').length).toBe(2) + expect(atom.config.get('some\\.key')[0]).toBe('one') + expect(atom.config.get('some\\.key')[1]).toBe('two') + }) + }) + + describe('::check for update', () => { + beforeEach(() => { + atom.config.unset('sync-settings._lastBackupHash') + }) + + it('updates last hash on backup', async () => { + await SyncSettings.backup() + + expect(atom.config.get('sync-settings._lastBackupHash')).toBeDefined() + }) + + it('updates last hash on restore', async () => { + await SyncSettings.restore() + + expect(atom.config.get('sync-settings._lastBackupHash')).toBeDefined() + }) + + describe('::notification', () => { + beforeEach(() => { + atom.notifications.clear() + }) + + it('displays on newer backup', async () => { + await SyncSettings.checkForUpdate() + + expect(atom.notifications.getNotifications().length).toBe(1) + expect(atom.notifications.getNotifications()[0].getType()).toBe('warning') + }) + + it('ignores on up-to-date backup', async () => { + await SyncSettings.backup() + atom.notifications.clear() + await SyncSettings.checkForUpdate() + + expect(atom.notifications.getNotifications().length).toBe(1) + expect(atom.notifications.getNotifications()[0].getType()).toBe('success') + }) + }) + }) + }) })