From 3cd91ffbb7ee8b4ef3a2b6efdc263966874400e3 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 12 Aug 2019 13:35:01 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9E=A1=EF=B8=8F=20Migrate=20core=20package?= =?UTF-8?q?=20'update-package-dependencies'=20into=20./packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 3 +- package.json | 4 +- packages/README.md | 3 +- .../update-package-dependencies/.gitignore | 3 + .../update-package-dependencies/LICENSE.md | 20 +++ .../update-package-dependencies/README.md | 5 + ...update-package-dependencies-status-view.js | 30 ++++ .../lib/update-package-dependencies.js | 81 +++++++++ .../update-package-dependencies/package.json | 39 +++++ .../spec/async-spec-helpers.js | 106 ++++++++++++ .../spec/update-package-dependencies-spec.js | 158 ++++++++++++++++++ .../styles/update-package-dependencies.less | 3 + 12 files changed, 449 insertions(+), 6 deletions(-) create mode 100644 packages/update-package-dependencies/.gitignore create mode 100644 packages/update-package-dependencies/LICENSE.md create mode 100644 packages/update-package-dependencies/README.md create mode 100644 packages/update-package-dependencies/lib/update-package-dependencies-status-view.js create mode 100644 packages/update-package-dependencies/lib/update-package-dependencies.js create mode 100644 packages/update-package-dependencies/package.json create mode 100644 packages/update-package-dependencies/spec/async-spec-helpers.js create mode 100644 packages/update-package-dependencies/spec/update-package-dependencies-spec.js create mode 100644 packages/update-package-dependencies/styles/update-package-dependencies.less diff --git a/package-lock.json b/package-lock.json index 877afdcd3fe..83c360d807a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6918,8 +6918,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "update-package-dependencies": { - "version": "https://www.atom.io/api/packages/update-package-dependencies/versions/0.13.1/tarball", - "integrity": "sha512-A0mvGI/fSHKsGPOTz/HVZ94UHUJOOJuwI4lAaauPeyZlEDHC2+VGoRDHcvLYxTyamumCGRhSVPDK3Gp8KItF9A==" + "version": "file:packages/update-package-dependencies" }, "user-home": { "version": "1.1.1", diff --git a/package.json b/package.json index 74b34277cdc..f4dba3c17a9 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "tree-sitter-css": "^0.13.7", "tree-view": "https://www.atom.io/api/packages/tree-view/versions/0.228.0/tarball", "typescript-simple": "1.0.0", - "update-package-dependencies": "https://www.atom.io/api/packages/update-package-dependencies/versions/0.13.1/tarball", + "update-package-dependencies": "file:./packages/update-package-dependencies", "vscode-ripgrep": "^1.2.5", "welcome": "file:packages/welcome", "whitespace": "https://www.atom.io/api/packages/whitespace/versions/0.37.7/tarball", @@ -225,7 +225,7 @@ "tabs": "0.110.0", "timecop": "0.36.2", "tree-view": "0.228.0", - "update-package-dependencies": "0.13.1", + "update-package-dependencies": "file:./packages/update-package-dependencies", "welcome": "file:./packages/welcome", "whitespace": "0.37.7", "wrap-guide": "0.41.0", diff --git a/packages/README.md b/packages/README.md index 806c5e9a2d9..ab510738118 100644 --- a/packages/README.md +++ b/packages/README.md @@ -96,7 +96,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **tabs** | [`atom/tabs`][tabs] | | | **timecop** | [`atom/timecop`][timecop] | [#18272](https://github.com/atom/atom/issues/18272) | | **tree-view** | [`atom/tree-view`][tree-view] | | -| **update-package-dependencies** | [`atom/update-package-dependencies`][update-package-dependencies] | [#18284](https://github.com/atom/atom/issues/18284) | +| **update-package-dependencies** | [`./update-package-dependencies`](./update-package-dependencies) | [#18284](https://github.com/atom/atom/issues/18284) | | **welcome** | [`./welcome`](./welcome) | [#18285](https://github.com/atom/atom/issues/18285) | | **whitespace** | [`atom/whitespace`][whitespace] | | | **wrap-guide** | [`atom/wrap-guide`][wrap-guide] | [#18286](https://github.com/atom/atom/issues/18286) | @@ -165,6 +165,5 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate [tabs]: https://github.com/atom/tabs [timecop]: https://github.com/atom/timecop [tree-view]: https://github.com/atom/tree-view -[update-package-dependencies]: https://github.com/atom/update-package-dependencies [whitespace]: https://github.com/atom/whitespace [wrap-guide]: https://github.com/atom/wrap-guide diff --git a/packages/update-package-dependencies/.gitignore b/packages/update-package-dependencies/.gitignore new file mode 100644 index 00000000000..ade14b9196f --- /dev/null +++ b/packages/update-package-dependencies/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +npm-debug.log +node_modules diff --git a/packages/update-package-dependencies/LICENSE.md b/packages/update-package-dependencies/LICENSE.md new file mode 100644 index 00000000000..4d231b4563b --- /dev/null +++ b/packages/update-package-dependencies/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2014 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/update-package-dependencies/README.md b/packages/update-package-dependencies/README.md new file mode 100644 index 00000000000..3b0fca9d547 --- /dev/null +++ b/packages/update-package-dependencies/README.md @@ -0,0 +1,5 @@ +## Update Package Dependencies package + +Runs `apm install` from the current project's directory. This will install all dependencies referenced in the `package.json` file to the `node_modules` folder. + +This should only be used in projects that are Atom packages. diff --git a/packages/update-package-dependencies/lib/update-package-dependencies-status-view.js b/packages/update-package-dependencies/lib/update-package-dependencies-status-view.js new file mode 100644 index 00000000000..45688d597c0 --- /dev/null +++ b/packages/update-package-dependencies/lib/update-package-dependencies-status-view.js @@ -0,0 +1,30 @@ +module.exports = class UpdatePackageDependenciesStatusView { + constructor(statusBar) { + this.statusBar = statusBar; + this.element = document.createElement('update-package-dependencies-status'); + this.element.classList.add( + 'update-package-dependencies-status', + 'inline-block', + 'is-read-only' + ); + this.spinner = document.createElement('span'); + this.spinner.classList.add( + 'loading', + 'loading-spinner-tiny', + 'inline-block' + ); + this.element.appendChild(this.spinner); + } + + attach() { + this.tile = this.statusBar.addRightTile({ item: this.element }); + this.tooltip = atom.tooltips.add(this.element, { + title: 'Updating package dependencies\u2026' + }); + } + + detach() { + if (this.tile) this.tile.destroy(); + if (this.tooltip) this.tooltip.dispose(); + } +}; diff --git a/packages/update-package-dependencies/lib/update-package-dependencies.js b/packages/update-package-dependencies/lib/update-package-dependencies.js new file mode 100644 index 00000000000..0c3d4e482d3 --- /dev/null +++ b/packages/update-package-dependencies/lib/update-package-dependencies.js @@ -0,0 +1,81 @@ +const { BufferedProcess } = require('atom'); +const UpdatePackageDependenciesStatusView = require('./update-package-dependencies-status-view'); + +module.exports = { + activate() { + this.subscription = atom.commands.add( + 'atom-workspace', + 'update-package-dependencies:update', + () => this.update() + ); + }, + + deactivate() { + this.subscription.dispose(); + if (this.updatePackageDependenciesStatusView) { + this.updatePackageDependenciesStatusView.detach(); + this.updatePackageDependenciesStatusView = null; + } + }, + + consumeStatusBar(statusBar) { + this.updatePackageDependenciesStatusView = new UpdatePackageDependenciesStatusView( + statusBar + ); + }, + + update() { + if (this.process) return; // Do not allow multiple apm processes to run + if (this.updatePackageDependenciesStatusView) + this.updatePackageDependenciesStatusView.attach(); + + let errorOutput = ''; + + const command = atom.packages.getApmPath(); + const args = ['install', '--no-color']; + const stderr = output => { + errorOutput += output; + }; + const options = { + cwd: this.getActiveProjectPath(), + env: Object.assign({}, process.env, { NODE_ENV: 'development' }) + }; + + const exit = code => { + this.process = null; + if (this.updatePackageDependenciesStatusView) + this.updatePackageDependenciesStatusView.detach(); + + if (code === 0) { + atom.notifications.addSuccess('Package dependencies updated'); + } else { + atom.notifications.addError('Failed to update package dependencies', { + detail: errorOutput, + dismissable: true + }); + } + }; + + this.process = this.runBufferedProcess({ + command, + args, + stderr, + exit, + options + }); + }, + + // This function exists so that it can be spied on by tests + runBufferedProcess(params) { + return new BufferedProcess(params); + }, + + getActiveProjectPath() { + const activeItem = atom.workspace.getActivePaneItem(); + if (activeItem && typeof activeItem.getPath === 'function') { + return atom.project.relativizePath(activeItem.getPath())[0]; + } else { + return atom.project.getPaths()[0]; + } + } +}; diff --git a/packages/update-package-dependencies/package.json b/packages/update-package-dependencies/package.json new file mode 100644 index 00000000000..46299757721 --- /dev/null +++ b/packages/update-package-dependencies/package.json @@ -0,0 +1,39 @@ +{ + "name": "update-package-dependencies", + "main": "./lib/update-package-dependencies", + "version": "0.13.1", + "private": true, + "description": "Runs `apm install` for the current project", + "repository": "https://github.com/atom/atom", + "license": "MIT", + "engines": { + "atom": ">0.39.0" + }, + "activationCommands": { + "atom-workspace": [ + "update-package-dependencies:update" + ] + }, + "consumedServices": { + "status-bar": { + "versions": { + "^1.1.0": "consumeStatusBar" + } + } + }, + "dependencies": {}, + "devDependencies": { + "standard": "^10.0.3" + }, + "standard": { + "env": { + "atomtest": true, + "browser": true, + "jasmine": true, + "node": true + }, + "globals": [ + "atom" + ] + } +} diff --git a/packages/update-package-dependencies/spec/async-spec-helpers.js b/packages/update-package-dependencies/spec/async-spec-helpers.js new file mode 100644 index 00000000000..f8d812c390f --- /dev/null +++ b/packages/update-package-dependencies/spec/async-spec-helpers.js @@ -0,0 +1,106 @@ +/** @babel */ + +export function beforeEach(fn) { + global.beforeEach(function() { + const result = fn(); + if (result instanceof Promise) { + waitsForPromise(() => result); + } + }); +} + +export function afterEach(fn) { + global.afterEach(function() { + const result = fn(); + if (result instanceof Promise) { + waitsForPromise(() => result); + } + }); +} + +['it', 'fit', 'ffit', 'fffit'].forEach(function(name) { + module.exports[name] = function(description, fn) { + if (fn === undefined) { + global[name](description); + return; + } + + global[name](description, function() { + const result = fn(); + if (result instanceof Promise) { + waitsForPromise(() => result); + } + }); + }; +}); + +export async function conditionPromise( + condition, + description = 'anonymous condition' +) { + const startTime = Date.now(); + + while (true) { + await timeoutPromise(100); + + if (await condition()) { + return; + } + + if (Date.now() - startTime > 5000) { + throw new Error('Timed out waiting on ' + description); + } + } +} + +export function timeoutPromise(timeout) { + return new Promise(function(resolve) { + global.setTimeout(resolve, timeout); + }); +} + +function waitsForPromise(fn) { + const promise = fn(); + global.waitsFor('spec promise to resolve', function(done) { + promise.then(done, function(error) { + jasmine.getEnv().currentSpec.fail(error); + done(); + }); + }); +} + +export function emitterEventPromise(emitter, event, timeout = 15000) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + reject(new Error(`Timed out waiting for '${event}' event`)); + }, timeout); + emitter.once(event, () => { + clearTimeout(timeoutHandle); + resolve(); + }); + }); +} + +export function promisify(original) { + return function(...args) { + return new Promise((resolve, reject) => { + args.push((err, ...results) => { + if (err) { + reject(err); + } else { + resolve(...results); + } + }); + + return original(...args); + }); + }; +} + +export function promisifySome(obj, fnNames) { + const result = {}; + for (const fnName of fnNames) { + result[fnName] = promisify(obj[fnName]); + } + return result; +} diff --git a/packages/update-package-dependencies/spec/update-package-dependencies-spec.js b/packages/update-package-dependencies/spec/update-package-dependencies-spec.js new file mode 100644 index 00000000000..dcce16cb85e --- /dev/null +++ b/packages/update-package-dependencies/spec/update-package-dependencies-spec.js @@ -0,0 +1,158 @@ +const os = require('os'); +const path = require('path'); +const updatePackageDependencies = require('../lib/update-package-dependencies'); + +const { + it, + fit, + ffit, + afterEach, + beforeEach +} = require('./async-spec-helpers'); // eslint-disable-line no-unused-vars + +describe('Update Package Dependencies', () => { + let projectPath = null; + + beforeEach(() => { + projectPath = __dirname; + atom.project.setPaths([projectPath]); + }); + + describe('updating package dependencies', () => { + let { command, args, stderr, exit, options } = {}; + beforeEach(() => { + spyOn(updatePackageDependencies, 'runBufferedProcess').andCallFake( + params => { + ({ command, args, stderr, exit, options } = params); + return true; // so that this.process isn't null + } + ); + }); + + afterEach(() => { + if (updatePackageDependencies.process) exit(0); + }); + + it('runs the `apm install` command', () => { + updatePackageDependencies.update(); + + expect(updatePackageDependencies.runBufferedProcess).toHaveBeenCalled(); + if (process.platform !== 'win32') { + expect(command.endsWith('/apm')).toBe(true); + } else { + expect(command.endsWith('\\apm.cmd')).toBe(true); + } + expect(args).toEqual(['install', '--no-color']); + expect(options.cwd).toEqual(projectPath); + }); + + it('only allows one apm process to be spawned at a time', () => { + updatePackageDependencies.update(); + expect(updatePackageDependencies.runBufferedProcess.callCount).toBe(1); + + updatePackageDependencies.update(); + updatePackageDependencies.update(); + expect(updatePackageDependencies.runBufferedProcess.callCount).toBe(1); + + exit(0); + updatePackageDependencies.update(); + expect(updatePackageDependencies.runBufferedProcess.callCount).toBe(2); + }); + + it('sets NODE_ENV to development in order to install devDependencies', () => { + updatePackageDependencies.update(); + + expect(options.env.NODE_ENV).toEqual('development'); + }); + + it('adds a status bar tile', async () => { + const statusBar = await atom.packages.activatePackage('status-bar'); + + const activationPromise = atom.packages.activatePackage( + 'update-package-dependencies' + ); + atom.commands.dispatch( + atom.views.getView(atom.workspace), + 'update-package-dependencies:update' + ); + const { mainModule } = await activationPromise; + + mainModule.update(); + + let tile = statusBar.mainModule.statusBar + .getRightTiles() + .find(tile => tile.item.matches('update-package-dependencies-status')); + expect( + tile.item.classList.contains('update-package-dependencies-status') + ).toBe(true); + expect(tile.item.firstChild.classList.contains('loading')).toBe(true); + + exit(0); + + tile = statusBar.mainModule.statusBar + .getRightTiles() + .find(tile => tile.item.matches('update-package-dependencies-status')); + expect(tile).toBeUndefined(); + }); + + describe('when there are multiple project paths', () => { + beforeEach(() => atom.project.setPaths([os.tmpdir(), projectPath])); + + it('uses the currently active one', async () => { + await atom.workspace.open(path.join(projectPath, 'package.json')); + + updatePackageDependencies.update(); + expect(options.cwd).toEqual(projectPath); + }); + }); + + describe('when the update succeeds', () => { + beforeEach(() => { + updatePackageDependencies.update(); + exit(0); + }); + + it('shows a success notification message', () => { + const notification = atom.notifications.getNotifications()[0]; + expect(notification.getType()).toEqual('success'); + expect(notification.getMessage()).toEqual( + 'Package dependencies updated' + ); + }); + }); + + describe('when the update fails', () => { + beforeEach(() => { + updatePackageDependencies.update(); + stderr('oh bother'); + exit(127); + }); + + it('shows a failure notification', () => { + const notification = atom.notifications.getNotifications()[0]; + expect(notification.getType()).toEqual('error'); + expect(notification.getMessage()).toEqual( + 'Failed to update package dependencies' + ); + expect(notification.getDetail()).toEqual('oh bother'); + expect(notification.isDismissable()).toBe(true); + }); + }); + }); + + describe('the `update-package-dependencies:update` command', () => { + beforeEach(() => spyOn(updatePackageDependencies, 'update')); + + it('activates the package and updates package dependencies', async () => { + const activationPromise = atom.packages.activatePackage( + 'update-package-dependencies' + ); + atom.commands.dispatch( + atom.views.getView(atom.workspace), + 'update-package-dependencies:update' + ); + const { mainModule } = await activationPromise; + expect(mainModule.update).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/update-package-dependencies/styles/update-package-dependencies.less b/packages/update-package-dependencies/styles/update-package-dependencies.less new file mode 100644 index 00000000000..16ac5739a54 --- /dev/null +++ b/packages/update-package-dependencies/styles/update-package-dependencies.less @@ -0,0 +1,3 @@ +.update-package-dependencies-status .loading.inline-block { + vertical-align: text-bottom; +}