Merge pull request #18104 from atom/migrate-incompatible-packages-package

➡️ Migrate core package 'incompatible-packages' into ./packages
This commit is contained in:
David Wilson
2018-09-26 06:38:56 -07:00
committed by GitHub
16 changed files with 729 additions and 5 deletions

3
package-lock.json generated
View File

@@ -2771,8 +2771,7 @@
}
},
"incompatible-packages": {
"version": "https://www.atom.io/api/packages/incompatible-packages/versions/0.27.3/tarball",
"integrity": "sha512-OlkFBSpvHH7dUfYQTlcgTXEa+sjr9Es8d2lNPGPS2O5Rp5MiRKcnovQoMtaF3fkcuV2O7onim45ldFjbl4qdog==",
"version": "file:packages/incompatible-packages",
"requires": {
"etch": "^0.12.2"
}

View File

@@ -73,7 +73,7 @@
"grammar-selector": "https://www.atom.io/api/packages/grammar-selector/versions/0.50.1/tarball",
"grim": "1.5.0",
"image-view": "https://www.atom.io/api/packages/image-view/versions/0.63.1/tarball",
"incompatible-packages": "https://www.atom.io/api/packages/incompatible-packages/versions/0.27.3/tarball",
"incompatible-packages": "file:packages/incompatible-packages",
"jasmine-json": "~0.0",
"jasmine-reporters": "1.1.0",
"jasmine-tagged": "^1.1.4",
@@ -207,7 +207,7 @@
"go-to-line": "0.33.0",
"grammar-selector": "0.50.1",
"image-view": "0.63.1",
"incompatible-packages": "0.27.3",
"incompatible-packages": "file:./packages/incompatible-packages",
"keybinding-resolver": "0.38.4",
"line-ending-selector": "0.7.7",
"link": "0.31.6",

View File

@@ -38,7 +38,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **go-to-line** | [`atom/go-to-line`][go-to-line] | [#17844](https://github.com/atom/atom/issues/17844) |
| **grammar-selector** | [`atom/grammar-selector`][grammar-selector] | [#17845](https://github.com/atom/atom/issues/17845) |
| **image-view** | [`atom/image-view`][image-view] | |
| **incompatible-packages** | [`atom/incompatible-packages`][incompatible-packages] | [#17846](https://github.com/atom/atom/issues/17846) |
| **incompatible-packages** | [`./packages/incompatible-packages`][./incompatible-packages] | [#17846](https://github.com/atom/atom/issues/17846) |
| **keybinding-resolver** | [`atom/keybinding-resolver`][keybinding-resolver] | |
| **language-c** | [`atom/language-c`][language-c] | |
| **language-clojure** | [`atom/language-clojure`][language-clojure] | |

View File

@@ -0,0 +1,3 @@
.DS_Store
npm-debug.log
node_modules

View File

@@ -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.

View File

@@ -0,0 +1,6 @@
# Incompatible Packages package
Displays a list of installed Atom packages that have native module
dependencies that are not compatible with the current version of Atom.
![](https://cloud.githubusercontent.com/assets/671378/3767534/3f099820-18ce-11e4-9fa0-feef7947aab2.png)

View File

@@ -0,0 +1,228 @@
/** @babel */
/** @jsx etch.dom */
import {BufferedProcess} from 'atom'
import etch from 'etch'
import VIEW_URI from './view-uri'
const REBUILDING = 'rebuilding'
const REBUILD_FAILED = 'rebuild-failed'
const REBUILD_SUCCEEDED = 'rebuild-succeeded'
export default class IncompatiblePackagesComponent {
constructor (packageManager) {
this.rebuildStatuses = new Map
this.rebuildFailureOutputs = new Map
this.rebuildInProgress = false
this.rebuiltPackageCount = 0
this.packageManager = packageManager
this.loaded = false
etch.initialize(this)
if (this.packageManager.getActivePackages().length > 0) {
this.populateIncompatiblePackages()
} else {
global.setImmediate(this.populateIncompatiblePackages.bind(this))
}
this.element.addEventListener('click', (event) => {
if (event.target === this.refs.rebuildButton) {
this.rebuildIncompatiblePackages()
} else if (event.target === this.refs.reloadButton) {
atom.reload()
} else if (event.target.classList.contains('view-settings')) {
atom.workspace.open(`atom://config/packages/${event.target.package.name}`)
}
})
}
update () {}
render () {
if (!this.loaded) {
return <div className='incompatible-packages padded'>Loading...</div>
}
return (
<div className='incompatible-packages padded native-key-bindings' tabIndex='-1'>
{this.renderHeading()}
{this.renderIncompatiblePackageList()}
</div>
)
}
renderHeading () {
if (this.incompatiblePackages.length > 0) {
if (this.rebuiltPackageCount > 0) {
let alertClass =
(this.rebuiltPackageCount === this.incompatiblePackages.length)
? 'alert-success icon-check'
: 'alert-warning icon-bug'
return (
<div className={'alert icon ' + alertClass}>
{this.rebuiltPackageCount} of {this.incompatiblePackages.length} packages
were rebuilt successfully. Reload Atom to activate them.
<button ref='reloadButton' className='btn pull-right'>
Reload Atom
</button>
</div>
)
} else {
return (
<div className='alert alert-danger icon icon-bug'>
Some installed packages could not be loaded because they contain native
modules that were compiled for an earlier version of Atom.
<button ref='rebuildButton' className='btn pull-right' disabled={this.rebuildInProgress}>
Rebuild Packages
</button>
</div>
)
}
} else {
return (
<div className='alert alert-success icon icon-check'>
None of your packages contain incompatible native modules.
</div>
)
}
}
renderIncompatiblePackageList () {
return (
<div>{
this.incompatiblePackages.map(this.renderIncompatiblePackage.bind(this))
}</div>
)
}
renderIncompatiblePackage (pack) {
let rebuildStatus = this.rebuildStatuses.get(pack)
return (
<div className={'incompatible-package'}>
{this.renderRebuildStatusIndicator(rebuildStatus)}
<button className='btn view-settings icon icon-gear pull-right' package={pack}>Package Settings</button>
<h4 className='heading'>
{pack.name} {pack.metadata.version}
</h4>
{
rebuildStatus
? this.renderRebuildOutput(pack)
: this.renderIncompatibleModules(pack)
}
</div>
)
}
renderRebuildStatusIndicator (rebuildStatus) {
if (rebuildStatus === REBUILDING) {
return (
<div className='badge badge-info pull-right icon icon-gear'>
Rebuilding
</div>
)
} else if (rebuildStatus === REBUILD_SUCCEEDED) {
return (
<div className='badge badge-success pull-right icon icon-check'>
Rebuild Succeeded
</div>
)
} else if (rebuildStatus === REBUILD_FAILED) {
return (
<div className='badge badge-error pull-right icon icon-x'>
Rebuild Failed
</div>
)
} else {
return ''
}
}
renderRebuildOutput (pack) {
if (this.rebuildStatuses.get(pack) === REBUILD_FAILED) {
return <pre>{this.rebuildFailureOutputs.get(pack)}</pre>
} else {
return ''
}
}
renderIncompatibleModules (pack) {
return (
<ul>{
pack.incompatibleModules.map((nativeModule) =>
<li>
<div className='icon icon-file-binary'>
{nativeModule.name}@{nativeModule.version || 'unknown'} <span className='text-warning'>{nativeModule.error}</span>
</div>
</li>
)
}</ul>
)
}
populateIncompatiblePackages () {
this.incompatiblePackages =
this.packageManager
.getLoadedPackages()
.filter(pack => !pack.isCompatible())
for (let pack of this.incompatiblePackages) {
let buildFailureOutput = pack.getBuildFailureOutput()
if (buildFailureOutput) {
this.setPackageStatus(pack, REBUILD_FAILED)
this.setRebuildFailureOutput(pack, buildFailureOutput)
}
}
this.loaded = true
etch.update(this)
}
async rebuildIncompatiblePackages () {
this.rebuildInProgress = true
let rebuiltPackageCount = 0
for (let pack of this.incompatiblePackages) {
this.setPackageStatus(pack, REBUILDING)
let {code, stderr} = await pack.rebuild()
if (code === 0) {
this.setPackageStatus(pack, REBUILD_SUCCEEDED)
rebuiltPackageCount++
} else {
this.setRebuildFailureOutput(pack, stderr)
this.setPackageStatus(pack, REBUILD_FAILED)
}
}
this.rebuildInProgress = false
this.rebuiltPackageCount = rebuiltPackageCount
etch.update(this)
}
setPackageStatus (pack, status) {
this.rebuildStatuses.set(pack, status)
etch.update(this)
}
setRebuildFailureOutput (pack, output) {
this.rebuildFailureOutputs.set(pack, output)
etch.update(this)
}
getTitle () {
return 'Incompatible Packages'
}
getURI () {
return VIEW_URI
}
getIconName () {
return 'package'
}
serialize () {
return {deserializer: 'IncompatiblePackagesComponent'}
}
}

View File

@@ -0,0 +1,52 @@
/** @babel */
import {Disposable, CompositeDisposable} from 'atom'
import VIEW_URI from './view-uri'
let disposables = null
export function activate () {
disposables = new CompositeDisposable()
disposables.add(atom.workspace.addOpener((uri) => {
if (uri === VIEW_URI) {
return deserializeIncompatiblePackagesComponent()
}
}))
disposables.add(atom.commands.add('atom-workspace', {
'incompatible-packages:view': () => {
atom.workspace.open(VIEW_URI)
}
}))
}
export function deactivate () {
disposables.dispose()
}
export function consumeStatusBar (statusBar) {
let incompatibleCount = 0
for (let pack of atom.packages.getLoadedPackages()) {
if (!pack.isCompatible()) incompatibleCount++
}
if (incompatibleCount > 0) {
let icon = createIcon(incompatibleCount)
let tile = statusBar.addRightTile({item: icon, priority: 200})
icon.element.addEventListener('click', () => {
atom.commands.dispatch(icon.element, 'incompatible-packages:view')
})
disposables.add(new Disposable(() => tile.destroy()))
}
}
export function deserializeIncompatiblePackagesComponent () {
const IncompatiblePackagesComponent = require('./incompatible-packages-component')
return new IncompatiblePackagesComponent(atom.packages)
}
function createIcon (count) {
const StatusIconComponent = require('./status-icon-component')
return new StatusIconComponent({count})
}

View File

@@ -0,0 +1,22 @@
/** @babel */
/** @jsx etch.dom */
import etch from 'etch'
export default class StatusIconComponent {
constructor ({count}) {
this.count = count
etch.initialize(this)
}
update () {}
render () {
return (
<div className='incompatible-packages-status inline-block text text-error'>
<span className='icon icon-bug'></span>
<span className='incompatible-packages-count'>{this.count}</span>
</div>
)
}
}

View File

@@ -0,0 +1,3 @@
/** @babel */
export default 'atom://incompatible-packages'

View File

@@ -0,0 +1,24 @@
{
"name": "incompatible-packages",
"main": "./lib/main",
"version": "0.27.3",
"description": "Show incompatible packages",
"repository": "https://github.com/atom/atom",
"license": "MIT",
"engines": {
"atom": ">0.50.0"
},
"dependencies": {
"etch": "^0.12.2"
},
"consumedServices": {
"status-bar": {
"versions": {
"^1.0.0": "consumeStatusBar"
}
}
},
"deserializers": {
"IncompatiblePackagesComponent": "deserializeIncompatiblePackagesComponent"
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "incompatible-package",
"version": "1.0.0",
"main": "./bad.js"
}

View File

@@ -0,0 +1,243 @@
/** @babel */
import etch from 'etch'
import IncompatiblePackagesComponent from '../lib/incompatible-packages-component'
describe('IncompatiblePackagesComponent', () => {
let packages, etchScheduler
beforeEach(() => {
etchScheduler = etch.getScheduler()
packages = [
{
name: 'incompatible-1',
isCompatible () {
return false
},
rebuild: function () {
return new Promise((resolve) => this.resolveRebuild = resolve)
},
getBuildFailureOutput () {
return null
},
path: '/Users/joe/.atom/packages/incompatible-1',
metadata: {
repository: 'https://github.com/atom/incompatible-1',
version: '1.0.0'
},
incompatibleModules: [
{name: 'x', version: '1.0.0', error: 'Expected version X, got Y'},
{name: 'y', version: '1.0.0', error: 'Expected version X, got Z'}
]
},
{
name: 'incompatible-2',
isCompatible () {
return false
},
rebuild () {
return new Promise((resolve) => this.resolveRebuild = resolve)
},
getBuildFailureOutput () {
return null
},
path: '/Users/joe/.atom/packages/incompatible-2',
metadata: {
repository: 'https://github.com/atom/incompatible-2',
version: '1.0.0'
},
incompatibleModules: [
{name: 'z', version: '1.0.0', error: 'Expected version X, got Y'}
]
},
{
name: 'compatible',
isCompatible () {
return true
},
rebuild () {
throw new Error('Should not rebuild a compatible package')
},
getBuildFailureOutput () {
return null
},
path: '/Users/joe/.atom/packages/b',
metadata: {
repository: 'https://github.com/atom/b',
version: '1.0.0'
},
incompatibleModules: [],
}
]
})
describe('when packages have not finished loading', () => {
it('delays rendering incompatible packages until the end of the tick', () => {
waitsForPromise(async () => {
let component =
new IncompatiblePackagesComponent({
getActivePackages: () => [],
getLoadedPackages: () => packages
})
let {element} = component
expect(element.querySelectorAll('.incompatible-package').length).toEqual(0)
await etchScheduler.getNextUpdatePromise()
expect(element.querySelectorAll('.incompatible-package').length).toBeGreaterThan(0)
})
})
})
describe('when there are no incompatible packages', () => {
it('does not render incompatible packages or the rebuild button', () => {
waitsForPromise(async () => {
expect(packages[2].isCompatible()).toBe(true)
let compatiblePackages = [packages[2]]
let component =
new IncompatiblePackagesComponent({
getActivePackages: () => compatiblePackages,
getLoadedPackages: () => compatiblePackages
})
let {element} = component
await etchScheduler.getNextUpdatePromise()
expect(element.querySelectorAll('.incompatible-package').length).toBe(0)
expect(element.querySelector('button')).toBeNull()
})
})
})
describe('when some packages previously failed to rebuild', () => {
it('renders them with failed build status and error output', () => {
waitsForPromise(async () => {
packages[1].getBuildFailureOutput = function () {
return 'The build failed'
}
let component =
new IncompatiblePackagesComponent({
getActivePackages: () => packages,
getLoadedPackages: () => packages
})
let {element} = component
await etchScheduler.getNextUpdatePromise()
let packageElement = element.querySelector('.incompatible-package:nth-child(2)')
expect(packageElement.querySelector('.badge').textContent).toBe('Rebuild Failed')
expect(packageElement.querySelector('pre').textContent).toBe('The build failed')
})
})
})
describe('when there are incompatible packages', () => {
it('renders incompatible packages and the rebuild button', () => {
waitsForPromise(async () => {
let component =
new IncompatiblePackagesComponent({
getActivePackages: () => packages,
getLoadedPackages: () => packages
})
let {element} = component
await etchScheduler.getNextUpdatePromise()
expect(element.querySelectorAll('.incompatible-package').length).toEqual(2)
expect(element.querySelector('button')).not.toBeNull()
})
})
describe('when the "Rebuild All" button is clicked', () => {
it('rebuilds every incompatible package, updating each package\'s view with status', () => {
waitsForPromise(async () => {
let component =
new IncompatiblePackagesComponent({
getActivePackages: () => packages,
getLoadedPackages: () => packages
})
let {element} = component
jasmine.attachToDOM(element)
await etchScheduler.getNextUpdatePromise()
component.refs.rebuildButton.dispatchEvent(new CustomEvent('click', {bubbles: true}))
await etchScheduler.getNextUpdatePromise() // view update
expect(component.refs.rebuildButton.disabled).toBe(true)
expect(packages[0].resolveRebuild).toBeDefined()
expect(element.querySelector('.incompatible-package:nth-child(1) .badge').textContent).toBe('Rebuilding')
expect(element.querySelector('.incompatible-package:nth-child(2) .badge')).toBeNull()
packages[0].resolveRebuild({code: 0}) // simulate rebuild success
await etchScheduler.getNextUpdatePromise() // view update
expect(packages[1].resolveRebuild).toBeDefined()
expect(element.querySelector('.incompatible-package:nth-child(1) .badge').textContent).toBe('Rebuild Succeeded')
expect(element.querySelector('.incompatible-package:nth-child(2) .badge').textContent).toBe('Rebuilding')
packages[1].resolveRebuild({code: 12, stderr: 'This is an error from the test!'}) // simulate rebuild failure
await etchScheduler.getNextUpdatePromise() // view update
expect(element.querySelector('.incompatible-package:nth-child(1) .badge').textContent).toBe('Rebuild Succeeded')
expect(element.querySelector('.incompatible-package:nth-child(2) .badge').textContent).toBe('Rebuild Failed')
expect(element.querySelector('.incompatible-package:nth-child(2) pre').textContent).toBe('This is an error from the test!')
})
})
it('displays a prompt to reload Atom when the packages finish rebuilding', () => {
waitsForPromise(async () => {
let component =
new IncompatiblePackagesComponent({
getActivePackages: () => packages,
getLoadedPackages: () => packages
})
let {element} = component
jasmine.attachToDOM(element)
await etchScheduler.getNextUpdatePromise() // view update
component.refs.rebuildButton.dispatchEvent(new CustomEvent('click', {bubbles: true}))
expect(packages[0].resolveRebuild({code: 0}))
await new Promise(global.setImmediate)
expect(packages[1].resolveRebuild({code: 0}))
await etchScheduler.getNextUpdatePromise() // view update
expect(component.refs.reloadButton).toBeDefined()
expect(element.querySelector('.alert').textContent).toMatch(/2 of 2/)
spyOn(atom, 'reload')
component.refs.reloadButton.dispatchEvent(new CustomEvent('click', {bubbles: true}))
expect(atom.reload).toHaveBeenCalled()
})
})
})
describe('when the "Package Settings" button is clicked', () => {
it('opens the settings panel for the package', () => {
waitsForPromise(async () => {
let component =
new IncompatiblePackagesComponent({
getActivePackages: () => packages,
getLoadedPackages: () => packages
})
let {element} = component
jasmine.attachToDOM(element)
await etchScheduler.getNextUpdatePromise()
spyOn(atom.workspace, 'open')
element.querySelector('.incompatible-package:nth-child(2) button').dispatchEvent(new CustomEvent('click', {bubbles: true}))
expect(atom.workspace.open).toHaveBeenCalledWith('atom://config/packages/incompatible-2')
})
})
})
})
})

View File

@@ -0,0 +1,77 @@
/** @babel */
import path from 'path'
import IncompatiblePackagesComponent from '../lib/incompatible-packages-component'
import StatusIconComponent from '../lib/status-icon-component'
// This exists only so that CI passes on both Atom 1.6 and Atom 1.8+.
function findStatusBar () {
if (typeof atom.workspace.getFooterPanels === 'function') {
const footerPanels = atom.workspace.getFooterPanels()
if (footerPanels.length > 0) {
return footerPanels[0].getItem()
}
}
return atom.workspace.getBottomPanels()[0].getItem()
}
describe('Incompatible packages', () => {
let statusBar
beforeEach(() => {
atom.views.getView(atom.workspace)
waitsForPromise(() => atom.packages.activatePackage('status-bar'))
runs(() => {
statusBar = findStatusBar()
})
})
describe('when there are packages with incompatible native modules', () => {
beforeEach(() => {
let incompatiblePackage = atom.packages.loadPackage(
path.join(__dirname, 'fixtures', 'incompatible-package')
)
spyOn(incompatiblePackage, 'isCompatible').andReturn(false)
incompatiblePackage.incompatibleModules = []
waitsForPromise(() => atom.packages.activatePackage("incompatible-packages"))
waits(1)
})
it('adds an icon to the status bar', () => {
let statusBarIcon = statusBar.getRightTiles()[0].getItem()
expect(statusBarIcon.constructor).toBe(StatusIconComponent)
})
describe('clicking the icon', () => {
it('displays the incompatible packages view in a pane', () => {
let statusBarIcon = statusBar.getRightTiles()[0].getItem()
statusBarIcon.element.dispatchEvent(new MouseEvent('click'))
let activePaneItem
waitsFor(() => (activePaneItem = atom.workspace.getActivePaneItem()))
runs(() => {
expect(activePaneItem.constructor).toBe(IncompatiblePackagesComponent)
})
})
})
})
describe('when there are no packages with incompatible native modules', () => {
beforeEach(() => {
waitsForPromise(() => atom.packages.activatePackage("incompatible-packages"))
})
it('does not add an icon to the status bar', () => {
let statusBarItemClasses = statusBar
.getRightTiles()
.map((tile) => tile.getItem().className)
expect(statusBarItemClasses).not.toContain('incompatible-packages')
})
})
})

View File

@@ -0,0 +1,42 @@
@import "ui-variables";
.incompatible-packages {
background-color: @pane-item-background-color;
overflow-y: scroll;
.incompatible-package {
padding: 15px;
margin-bottom: 10px;
border-radius: 6px;
border: 1px solid #d1d1d2;
background-color: #fafafa;
overflow: hidden;
.badge {
margin-left: 1em;
}
.heading {
margin-top: 0px;
}
ul {
padding-left: 1em;
}
li {
list-style-type: none;
}
pre {
margin-top: 2em;
max-height: 25em;
overflow: scroll;
color: @text-color-error;
}
}
}
.incompatible-packages-status {
padding-left: 2px;
}