├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── appveyor.yml ├── create-provider.md ├── keymaps └── build.json ├── lib ├── atom-build.js ├── build-error.js ├── build-view.js ├── build.js ├── config.js ├── error-matcher.js ├── google-analytics.js ├── linter-integration.js ├── save-confirm-view.js ├── status-bar-view.js ├── target-manager.js ├── targets-view.js └── utils.js ├── menus └── build.json ├── package.json ├── spec ├── .eslintrc ├── build-atomCommandName-spec.js ├── build-confirm-spec.js ├── build-error-match-spec.js ├── build-hooks-spec.js ├── build-keymap-spec.js ├── build-spec.js ├── build-targets-spec.js ├── build-view-spec.js ├── build-visible-spec.js ├── custom-provider-spec.js ├── fixture │ ├── .atom-build.cson │ ├── .atom-build.error-match-function.js │ ├── .atom-build.error-match-long-output.json │ ├── .atom-build.error-match-multiple-errorMatch.json │ ├── .atom-build.error-match-multiple-first.json │ ├── .atom-build.error-match-multiple.json │ ├── .atom-build.error-match-no-exit1.json │ ├── .atom-build.error-match-no-file.json │ ├── .atom-build.error-match-no-line-col.json │ ├── .atom-build.error-match.json │ ├── .atom-build.error-match.message.json │ ├── .atom-build.js │ ├── .atom-build.json │ ├── .atom-build.match-function-change-dirs.js │ ├── .atom-build.match-function-html.js │ ├── .atom-build.match-function-message-and-html.js │ ├── .atom-build.match-function-trace-html.js │ ├── .atom-build.match-function-trace-message-and-html.js │ ├── .atom-build.match-function-trace.js │ ├── .atom-build.match-function-warning.js │ ├── .atom-build.replace.json │ ├── .atom-build.sh-default.json │ ├── .atom-build.sh-false.json │ ├── .atom-build.sh-true.json │ ├── .atom-build.shell.json │ ├── .atom-build.syntax-error.json │ ├── .atom-build.targets.json │ ├── .atom-build.warning-match.json │ ├── .atom-build.yml │ ├── atom-build-hooks-dummy-package │ │ ├── main.js │ │ └── package.json │ ├── atom-build-spec-linter │ │ ├── atom-build-spec-linter.js │ │ └── package.json │ └── change_dir_output.txt ├── helpers.js ├── linter-intergration-spec.js └── utils-spec.js └── styles ├── animations.less ├── build.less ├── panel-left-right.less └── panel-top-bottom.less /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "new-cap": [ 2, { "capIsNewExceptions": [ "XRegExp" ] } ] 5 | }, 6 | "extends": "atom-build" 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | /node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: 3 | on_success: never 4 | on_failure: change 5 | 6 | webhooks: 7 | urls: 8 | - https://webhooks.gitter.im/e/de0569306a16f2435ef2 9 | on_success: change 10 | on_failure: always 11 | on_start: false 12 | 13 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 14 | 15 | git: 16 | depth: 10 17 | 18 | sudo: false 19 | 20 | os: 21 | - osx 22 | 23 | env: 24 | global: 25 | - APM_TEST_PACKAGES="" 26 | 27 | matrix: 28 | - ATOM_CHANNEL=stable 29 | - ATOM_CHANNEL=beta 30 | 31 | addons: 32 | apt: 33 | packages: 34 | - build-essential 35 | - git 36 | - libgnome-keyring-dev 37 | - fakeroot 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Alexander Olsson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atom Build package 2 | 3 | [Release notes](https://github.com/noseglid/atom-build/releases) 4 | 5 | [![Plugin installs](https://img.shields.io/apm/dm/build.svg?style=flat-square)](https://atom.io/packages/build) 6 | [![Package version](https://img.shields.io/apm/v/build.svg?style=flat-square)](https://atom.io/packages/build) 7 | 8 | [![Travis.ci Shield](https://img.shields.io/travis/noseglid/atom-build/master.svg?style=flat-square&label=travis%20ci)](https://travis-ci.org/noseglid/atom-build) 9 | [![AppVeyor Shield](https://img.shields.io/appveyor/ci/noseglid/atom-build/master.svg?style=flat-square&label=appveyor )](https://ci.appveyor.com/project/noseglid/atom-build) 10 | 11 | [![Gitter chat](https://img.shields.io/badge/gitter-noseglid%2Fatom--build-24CE66.svg?style=flat-square)](https://gitter.im/noseglid/atom-build) 12 | [![Slack Badge](https://img.shields.io/badge/chat-atom.io%20slack-ff69b4.svg?style=flat-square)](http://atom-slack.herokuapp.com/) 13 | 14 | 15 | Automatically build your project inside your new favorite editor, Atom. 16 | 17 | * Cmd Alt B / Ctrl Alt B / F9 builds your project. 18 | * Cmd Alt G / Ctrl Alt G / F4 cycles through causes of build error. See [Error Matching](#error-match). 19 | * Cmd Alt G / Ctrl Alt H / Shift F4 goes to the first build error. See [Error Matching](#error-match). 20 | * Cmd Alt V / Ctrl Alt V / F8 Toggles the build panel. 21 | * Cmd Alt T / Ctrl Alt T / F7 Displays the available build targets. 22 | * Esc terminates build / closes the build window. 23 | 24 | #### Builds your project - configure it your way 25 | ![work work](https://noseglid.github.io/build.gif) 26 | 27 | #### Automatically extract targets - here with [build-make](https://github.com/AtomBuild/atom-build-make). 28 | ![targets](https://noseglid.github.io/targets-make.gif) 29 | 30 | #### Match errors and go directly to offending code - with [Atom Linter](https://atom.io/packages/linter). 31 | ![error matching](https://noseglid.github.io/error-match.gif) 32 | 33 | (You can also use keyboard shortcuts to go to errors if you don't like Atom Linter, or want to keep package dependencies to a minimum). 34 | 35 | ### Quick start 36 | 37 | Install the build package using apm (apm can be installed using the install shell commands tool in Atom)(Linux/Mac): 38 | ```bash 39 | $ apm install build 40 | ``` 41 | 42 | Create a file called `.atom-build.yml` (note the inital dot): 43 | ```yml 44 | cmd: echo Hello world 45 | ``` 46 | 47 | Save it, and press Cmd Alt B (OS X) / Ctrl Alt B (Linux/Windows) 48 | and you should see the output of `echo Hello world`, which should be `Hello world` if all is correct. 49 | 50 | ## Build providers 51 | 52 | Instead of specifying commands manually, you can use a build provider. They often include functionality such as parsing 53 | targets (for instance all tasks from `gulpfile.js` or `Makefile`). 54 | 55 | **[Full list of build providers](https://atombuild.github.io)** 56 | 57 | 58 | ### Specify a custom build command 59 | 60 | If no build provider is enough to suit your needs, you can configure the custom build command extensively. 61 | 62 | Supported formats and the name of the configuration file is 63 | 64 | * JSON: `.atom-build.json` 65 | * CSON: `.atom-build.cson` 66 | * YAML: `.atom-build.yaml` or `.atom-build.yml` 67 | * JS: `.atom-build.js` 68 | 69 | Pick your favorite format, save that file in your project root, and specify exactly 70 | how your project is built (example in `yml`) 71 | 72 | ```yml 73 | cmd: "" 74 | name: "" 75 | args: 76 | - 77 | - 78 | sh: true, 79 | cwd: 80 | env: 81 | VARIABLE1: "VALUE1" 82 | VARIABLE2: "VALUE2" 83 | errorMatch: 84 | - ^regexp1$ 85 | - ^regexp2$ 86 | warningMatch: 87 | - ^regexp1$ 88 | - ^regexp2$ 89 | keymap: 90 | atomCommandName: namespace:command 91 | targets: 92 | extraTargetName: 93 | cmd: "" 94 | args: 95 | # (any previous options are viable here except `targets` itself) 96 | ``` 97 | 98 | Note that if `sh` is false `cmd` must only be the executable - no arguments here. If the 99 | executable is not in your path, either fully qualify it or specify the path 100 | in you environment (e.g. by setting the `PATH` var appropriately on UNIX-like 101 | systems). 102 | 103 | If `sh` is true, it will use a shell (e.g. `/bin/sh -c`) on unix/linux, and command (`cmd /S /C`) 104 | on windows. 105 | 106 | #### Programmatic Build commands (Javascript) 107 | 108 | Using a JavaScript (JS) file gives you the additional benefit of being able to specify `preBuild` and `postBuild` and being able to run arbitrary match functions instead of regex-matching. The 109 | javascript function needs to return an array of matches. The fields of the matches must be the same 110 | as those that the regex can set. 111 | 112 | Keep in mind that the JavaScript file must `export` the configuration 113 | 114 | ```javascript 115 | module.exports = { 116 | cmd: "myCommand", 117 | preBuild: function () { 118 | console.log('This is run **before** the build command'); 119 | }, 120 | postBuild: function () { 121 | console.log('This is run **after** the build command'); 122 | }, 123 | functionMatch: function (terminal_output) { 124 | // this is the array of matches that we create 125 | var matches = []; 126 | terminal_output.split(/\n/).forEach(function (line, line_number, terminal_output) { 127 | // all lines starting with a slash 128 | if line[0] == '/' { 129 | this.push({ 130 | file: 'x.txt', 131 | line: line_number.toString(), 132 | message: line 133 | }); 134 | } 135 | }.bind(matches)); 136 | return matches; 137 | } 138 | }; 139 | ``` 140 | 141 | A major advantage of the `functionMatch` method is that you can keep state while 142 | parsing the output. For example, if you have a `Makefile` output like this: 143 | 144 | ```terminal 145 | make[1]: Entering directory 'foo' 146 | make[2]: Entering directory 'foo/src' 147 | testmake.c: In function 'main': 148 | testmake.c:3:5: error: unknown type name 'error' 149 | ``` 150 | 151 | then you can't use a regex to match the filename, because the regex doesn't have 152 | the information about the directory changes. The following `functionMatch` can 153 | handle this case. Explanations are in the comments: 154 | 155 | ```js 156 | module.exports = { 157 | cmd: 'make', 158 | name: 'Makefile', 159 | sh: true, 160 | functionMatch: function (output) { 161 | const enterDir = /^make\[\d+\]: Entering directory '([^']+)'$/; 162 | const error = /^([^:]+):(\d+):(\d+): error: (.+)$/; 163 | // this is the list of error matches that atom-build will process 164 | const array = []; 165 | // stores the current directory 166 | var dir = null; 167 | // iterate over the output by lines 168 | output.split(/\r?\n/).forEach(line => { 169 | // update the current directory on lines with `Entering directory` 170 | const dir_match = enterDir.exec(line); 171 | if (dir_match) { 172 | dir = dir_match[1]; 173 | } else { 174 | // process possible error messages 175 | const error_match = error.exec(line); 176 | if (error_match) { 177 | // map the regex match to the error object that atom-build expects 178 | array.push({ 179 | file: dir ? dir + '/' + error_match[1] : error_match[1], 180 | line: error_match[2], 181 | col: error_match[3], 182 | message: error_match[4] 183 | }); 184 | } 185 | } 186 | }); 187 | return array; 188 | } 189 | }; 190 | ``` 191 | 192 | Another feature of `functionMatch` is that you can attach informational messages 193 | to the error messages: 194 | 195 | ![pic of traces and custom error types](https://cloud.githubusercontent.com/assets/332036/15097688/ddfc170c-1523-11e6-8394-d24a79d125ea.png) 196 | 197 | You can add these additional messages by setting the trace field of the error 198 | object. It needs to be an array of objects with the same fields as the error. 199 | Instead of adding squiggly lines at the location given by the `file`, `line` and 200 | `col` fields, a link is added to the popup message, so you can conveniently jump 201 | to the location given in the trace. 202 | 203 | One more feature provided by `functionMatch` is the ability to use HTML in 204 | the message text by setting `html_message` instead of `message`. If both 205 | `html_message` and `message` are set, the latter takes priority. 206 | 207 | 208 | #### Configuration options 209 | 210 | Option | Required | Description 211 | ------------------|----------------|----------------------- 212 | `cmd` | **[required]** | The executable command 213 | `name` | *[optional]* | The name of the target. Viewed in the targets list (toggled by `build:select-active-target`). 214 | `args` | *[optional]* | An array of arguments for the command 215 | `sh` | *[optional]* | If `true`, the combined command and arguments will be passed to `/bin/sh`. Default `true`. 216 | `cwd` | *[optional]* | The working directory for the command. E.g. what `.` resolves to. 217 | `env` | *[optional]* | An object of environment variables and their values to set 218 | `errorMatch` | *[optional]* | A (list of) regular expressions to match output to a file, row and col. See [Error matching](#error-match) for details. 219 | `warningMatch` | *[optional]* | Like `errorMatch`, but is reported as just a warning 220 | `functionMatch` | *[optional]* | A (list of) javascript functions that return a list of match objects 221 | `keymap` | *[optional]* | A keymap string as defined by [`Atom`](https://atom.io/docs/latest/behind-atom-keymaps-in-depth). Pressing this key combination will trigger the target. Examples: `ctrl-alt-k` or `cmd-U`. 222 | `killSignals` | *[optional]* | An array of signals. The signals will be sent, one after each time `Escape` is pressed until the process has been terminated. The default value is `SIGINT` -> `SIGTERM` -> `SIGKILL`. The only signal which is guaranteed to terminate the process is `SIGKILL` so it is recommended to include that in the list. 223 | `atomCommandName` | *[optional]* | Command name to register which should be on the form of `namespace:command`. Read more about [Atom CommandRegistry](https://atom.io/docs/api/v1.4.1/CommandRegistry). The command will be available in the command palette and can be trigger from there. If this is returned by a build provider, the command can programatically be triggered by [dispatching](https://atom.io/docs/api/v1.4.1/CommandRegistry#instance-dispatch). 224 | `targets` | *[optional]* | Additional targets which can be used to build variations of your project. 225 | `preBuild` | *[optional]* | **JS only**. A function which will be called *before* executing `cmd`. No arguments. `this` will be the build configuration. 226 | `postBuild` | *[optional]* | **JS only**. A function which will be called *after* executing `cmd`. It will be passed 3 arguments: `bool buildOutcome` indicating outcome of the running `cmd`, `string stdout` containing the contents of `stdout`, and `string stderr` containing the contents of `stderr`. `this` will be the build configuration. 227 | 228 | #### Replacements 229 | 230 | The following parameters will be replaced in `cmd`, any entry in `args`, `cwd` and 231 | values of `env`. They should all be enclosed in curly brackets `{}` 232 | 233 | * `{FILE_ACTIVE}` - Full path to the currently active file in Atom. E.g. `/home/noseglid/github/atom-build/lib/build.js` 234 | * `{FILE_ACTIVE_PATH}` - Full path to the folder where the currently active file is. E.g. `/home/noseglid/github/atom-build/lib` 235 | * `{FILE_ACTIVE_NAME}` - Full name and extension of active file. E.g., `build.js` 236 | * `{FILE_ACTIVE_NAME_BASE}` - Name of active file WITHOUT extension. E.g., `build` 237 | * `{FILE_ACTIVE_CURSOR_ROW}` - Line number where the last inserted cursor sits. E.g, `21` 238 | * `{FILE_ACTIVE_CURSOR_COLUMN}` - Column number where the last inserted cursor sits. E.g, `42` 239 | * `{PROJECT_PATH}` - Full path to the root of the project. This is normally the path Atom has as root. E.g `/home/noseglid/github/atom-build` 240 | * `{REPO_BRANCH_SHORT}` - Short name of the current active branch (if project is backed by git). E.g `master` or `v0.9.1` 241 | * `{SELECTION}` - Selected text. 242 | 243 | ### Creating a build provider 244 | Creating a build provider require very little code in the easiest case, and can 245 | be as complicated as necessary to achieve the correct functionality. 246 | Read more about building your own provider in [the create provider documentation](create-provider.md). 247 | 248 | 249 | ## Error matching 250 | 251 | Error matching lets you specify a single regular expression or a list of 252 | regular expressions, which capture the output of your build command and open the 253 | correct file, row and column of the error. For instance: 254 | 255 | ```bash 256 | ../foo/bar/a.c:4:26: error: expected ';' after expression 257 | printf("hello world\n") 258 | ^ 259 | ; 260 | 1 error generated. 261 | ``` 262 | 263 | Would be matched with the regular expression: `(?[\\/0-9a-zA-Z\\._]+):(?\\d+):(?\\d+):\\s+(?.+)`. 264 | After the build has failed, pressing Cmd Alt G (OS X) or Ctrl Alt G (Linux/Windows) 265 | (or F4 on either platform), `a.c` would be opened and the cursor would be placed at row 4, column 26. 266 | 267 | Note the syntax for match groups. This is from the [XRegExp](http://xregexp.com/) package 268 | and has the syntax for named groups: `(? RE )` where `name` would be the name of the group 269 | matched by the regular expression `RE`. 270 | 271 | The following named groups can be matched from the output: 272 | * `file` - **[required]** the file to open. May be relative `cwd` or absolute. `(? RE)`. 273 | * `line` - *[optional]* the line the error starts on. `(? RE)`. 274 | * `col` - *[optional]* the column the error starts on. `(? RE)`. 275 | * `line_end` - *[optional]* the line the error ends on. `(? RE)`. 276 | * `col_end` - *[optional]* the column the error ends on. `(? RE)`. 277 | * `message` - *[optional]* Catch the humanized error message. `(? RE)`. 278 | 279 | The `file` should be relative the `cwd` specified. If no `cwd` has been specified, then 280 | the `file` should be relative the project root (e.g. the top most directory shown in the 281 | Atom Editor). 282 | 283 | If your build outputs multiple errors, all will be matched. Press Cmd Alt G (OS X) or Ctrl Alt G (Linux/Windows) 284 | to cycle through the errors (in the order they appear, first on stderr then on stdout), or you can use the 285 | Atom Linter integration discussed in the next section. 286 | 287 | Often, the first error is the most interesting since other errors tend to be secondary faults caused by that first one. 288 | To jump to the first error you can use Cmd Alt H (OS X) or Shift F4 (Linux/Windows) at any point to go to the first error. 289 | 290 | ### Error matching and Atom Linter 291 | 292 | Install [Atom Linter](https://atom.io/packages/linter) and all your matched errors will listed in a neat panel. 293 | 294 | ![Linter integration](https://noseglid.github.io/build-linter.png) 295 | 296 | ## Analytics 297 | 298 | The `atom-build` package uses google analytics to keep track of which features are in use 299 | and at what frequency. This gives the maintainers a sense of what parts of the 300 | package is most important and what parts can be removed. 301 | 302 | The data is fully anonymous and can not be tracked back to you in any way. 303 | This is what is collected 304 | 305 | * Version of package used. 306 | * Build triggered, succeeded or failed. 307 | * Which build tool was used. 308 | * Visibility of UI components. 309 | 310 | If you really do not want to share this information, you can opt out by disabling 311 | the [metrics package](https://atom.io/packages/metrics). This will disable all analytics 312 | collection, including the one from `atom-build`. 313 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | platform: x64 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | skip_tags: true 10 | 11 | environment: 12 | APM_TEST_PACKAGES: 13 | 14 | matrix: 15 | - ATOM_CHANNEL: stable 16 | - ATOM_CHANNEL: beta 17 | 18 | install: 19 | - ps: Install-Product node 5 20 | 21 | build_script: 22 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) 23 | 24 | test: off 25 | deploy: off 26 | -------------------------------------------------------------------------------- /create-provider.md: -------------------------------------------------------------------------------- 1 | # Creating a build provider 2 | 3 | ## Service API 4 | 5 | Another package may provide build information to the `build`-package by implementing its service API. 6 | The package should integrate via the service API. This is typically done in `package.json`: 7 | 8 | ```javascript 9 | { 10 | "providedServices": { 11 | "builder": { 12 | "description": "Description of the build configurations this package gives", 13 | "versions": { 14 | "2.0.0": "providingFunction" 15 | } 16 | } 17 | } 18 | }, 19 | ``` 20 | 21 | The `build`-package will then call `providingFunction` when activated and expects an 22 | ES6 class or an array of classes in return. The next section describes in detail how that class is 23 | expected to operate. 24 | 25 | ## The provider implementation 26 | ```javascript 27 | class MyBuildProvider { 28 | 29 | constructor(cwd) { 30 | // OPTIONAL: setup here 31 | // cwd is the project root this provider will operate in, so store `cwd` in `this`. 32 | } 33 | 34 | destructor() { 35 | // OPTIONAL: tear down here. 36 | // destructor is not part of ES6. This is called by `build` like any 37 | // other method before deactivating. 38 | return 'void'; 39 | } 40 | 41 | getNiceName() { 42 | // REQUIRED: return a nice readable name of this provider. 43 | return 'string'; 44 | } 45 | 46 | isEligible() { 47 | // REQUIRED: Perform operations to determine if this build provider can 48 | // build the project in `cwd` (which was specified in `constructor`). 49 | return 'boolean'; 50 | } 51 | 52 | settings() { 53 | // REQUIRED: Return an array of objects which each define a build description. 54 | return 'array of objects'; // [ { ... }, { ... }, ] 55 | } 56 | 57 | on(event, cb) { 58 | // OPTIONAL: The build provider can let `build` know when it is time to 59 | // refresh targets. 60 | return 'void'; 61 | } 62 | 63 | removeAllListeners(event) { 64 | // OPTIONAL: (required if `on` is defined) removes all listeners registered in `on` 65 | return 'void'; 66 | } 67 | } 68 | ``` 69 | 70 | `constructor` _[optional]_ - is used in ES6 classes to initialize the class. The path 71 | where this instance of the build provider will operate is provided. 72 | Please note that the build provider will be instanced once per project folder. 73 | 74 | --- 75 | 76 | `destructor` _[optional]_ - will be called before `build` is deactivated and gives you a chance 77 | to release any resources claimed. 78 | 79 | --- 80 | 81 | `getNiceName` - aesthetic only and should be a `string` which is a human readable 82 | description of the build configuration is provided. 83 | 84 | --- 85 | 86 | `isEligible` - should be a function which must return synchronously. 87 | It should return `true` or `false` indicating if it can build the folder specified 88 | in the constructor into something sensible. Typically look for the existence of a 89 | build file such as `gulpfile.js` or `Makefile`. 90 | 91 | --- 92 | 93 | `settings` - can return a Promise or an array of objects. 94 | It can provide anything which is allowed by the [custom build configuration](README.md#custom-build-config). 95 | This includes the command, `cmd`, to execute, any arguments, `args`, and so on. 96 | 97 | --- 98 | 99 | `on` _[optional]_ - will be called with a string which is the name of an event the build tool provider can emit. The build 100 | tool provider should call the `callback` when the specified event occurs. 101 | The easiest way to use this is to extends [NodeJS's event emitter](https://nodejs.org/api/events.html#events_class_events_eventemitter) and simply issue `this.emit(event)`. 102 | Events `build` may ask for include: 103 | * `refresh` - call the callback if you want `build` to refresh all targets. 104 | this is common after the build file has been altered. 105 | 106 | Note: If you extend `EventEmitter` you don't need to implement this method. 107 | 108 | --- 109 | 110 | `removeAllListeners` _[optional]_ - will be called when `build` is no longer interested in that event. It may be because 111 | `build` is being deactivated, or refreshing its state. `build` will never call `removeAllListeners` for an event unless it has 112 | previously registered a listener via `on` first. 113 | 114 | Note: If you extend `EventEmitter` you don't need to implement this method. 115 | 116 | ## Operations 117 | 118 | Before `settings` is called, the build provider will always be given a chance to 119 | let `build` know if it can do anything with the project folder by returning `true` or `false` 120 | from the `isEligible` method. Checks for eligibility should always be performed here as 121 | the content of the project folder may have changed between two calls. 122 | 123 | `build` will refresh targets for a variety of events: 124 | * Atom is started, `build` will instance one build provider for every project root 125 | folder in Atom and ask for build targets. 126 | * A project root folder is added, `build` will instance a new build provider for this 127 | folder and ask for build targets. 128 | * Any build provider emits the `refresh`, in which case all providers in that folder 129 | will be asked for targets. 130 | * The user (or any other package) issues `build:refresh-targets` (e.g. via the command palette). 131 | 132 | ## Publication 133 | 134 | If you want to share your provider (please do, if you needed it, chances are others will as well) 135 | go to the [AtomBuild project for the homepage](https://github.com/AtomBuild/atombuild.github.io) 136 | and follow the instructions there. Your tool will then be visisble on [https://atombuild.github.io](https://atombuild.github.io). This is also a great source to look 137 | for existing build providers 138 | 139 | --- 140 | 141 | Happy coding! 142 | -------------------------------------------------------------------------------- /keymaps/build.json: -------------------------------------------------------------------------------- 1 | { 2 | ".platform-darwin atom-workspace, .platform-darwin atom-text-editor": { 3 | "cmd-alt-b": "build:trigger", 4 | "cmd-alt-v": "build:toggle-panel", 5 | "cmd-alt-g": "build:error-match", 6 | "cmd-alt-h": "build:error-match-first", 7 | "cmd-alt-t": "build:select-active-target" 8 | }, 9 | ".platform-linux atom-workspace, .platform-linux atom-text-editor, .platform-win32 atom-workspace, .platform-win32 atom-text-editor": { 10 | "ctrl-alt-b": "build:trigger", 11 | "ctrl-alt-v": "build:toggle-panel", 12 | "ctrl-alt-g": "build:error-match", 13 | "ctrl-alt-h": "build:error-match-first", 14 | "ctrl-alt-t": "build:select-active-target" 15 | }, 16 | "atom-workspace, atom-text-editor": { 17 | "f9": "build:trigger", 18 | "f8": "build:toggle-panel", 19 | "f4": "build:error-match", 20 | "shift-f4": "build:error-match-first", 21 | "f7": "build:select-active-target" 22 | }, 23 | ".build": { 24 | "escape": "build:stop" 25 | }, 26 | ".build-confirm": { 27 | "enter": "build:confirm", 28 | "escape": "build:no-confirm" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/atom-build.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import EventEmitter from 'events'; 4 | 5 | function getConfig(file) { 6 | const fs = require('fs'); 7 | const realFile = fs.realpathSync(file); 8 | delete require.cache[realFile]; 9 | switch (require('path').extname(file)) { 10 | case '.json': 11 | case '.js': 12 | return require(realFile); 13 | 14 | case '.cson': 15 | return require('cson-parser').parse(fs.readFileSync(realFile)); 16 | 17 | case '.yaml': 18 | case '.yml': 19 | return require('js-yaml').safeLoad(fs.readFileSync(realFile)); 20 | } 21 | 22 | return {}; 23 | } 24 | 25 | function createBuildConfig(build, name) { 26 | const conf = { 27 | name: 'Custom: ' + name, 28 | exec: build.cmd, 29 | env: build.env, 30 | args: build.args, 31 | cwd: build.cwd, 32 | sh: build.sh, 33 | errorMatch: build.errorMatch, 34 | functionMatch: build.functionMatch, 35 | warningMatch: build.warningMatch, 36 | atomCommandName: build.atomCommandName, 37 | keymap: build.keymap, 38 | killSignals: build.killSignals 39 | }; 40 | 41 | if (typeof build.postBuild === 'function') { 42 | conf.postBuild = build.postBuild; 43 | } 44 | 45 | if (typeof build.preBuild === 'function') { 46 | conf.preBuild = build.preBuild; 47 | } 48 | 49 | return conf; 50 | } 51 | 52 | export default class CustomFile extends EventEmitter { 53 | constructor(cwd) { 54 | super(); 55 | this.cwd = cwd; 56 | this.fileWatchers = []; 57 | } 58 | 59 | destructor() { 60 | this.fileWatchers.forEach(fw => fw.close()); 61 | } 62 | 63 | getNiceName() { 64 | return 'Custom file'; 65 | } 66 | 67 | isEligible() { 68 | const os = require('os'); 69 | const fs = require('fs'); 70 | const path = require('path'); 71 | this.files = [].concat.apply([], [ 'json', 'cson', 'yaml', 'yml', 'js' ].map(ext => [ 72 | path.join(this.cwd, `.atom-build.${ext}`), 73 | path.join(os.homedir(), `.atom-build.${ext}`) 74 | ])).filter(fs.existsSync); 75 | return 0 < this.files.length; 76 | } 77 | 78 | settings() { 79 | const fs = require('fs'); 80 | this.fileWatchers.forEach(fw => fw.close()); 81 | // On Linux, closing a watcher triggers a new callback, which causes an infinite loop 82 | // fallback to `watchFile` here which polls instead. 83 | this.fileWatchers = this.files.map(file => 84 | (require('os').platform() === 'linux' ? fs.watchFile : fs.watch)(file, () => this.emit('refresh')) 85 | ); 86 | 87 | const config = []; 88 | this.files.map(getConfig).forEach(build => { 89 | config.push( 90 | createBuildConfig(build, build.name || 'default'), 91 | ...Object.keys(build.targets || {}).map(name => createBuildConfig(build.targets[name], name)) 92 | ); 93 | }); 94 | 95 | return config; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/build-error.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | export default class BuildError extends Error { 4 | constructor(name, message) { 5 | super(message); 6 | this.name = name; 7 | this.message = message; 8 | Error.captureStackTrace(this, BuildError); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/build-view.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { View, $ } from 'atom-space-pen-views'; 4 | 5 | export default class BuildView extends View { 6 | 7 | static initialTimerText() { 8 | return '0.0 s'; 9 | } 10 | 11 | static initialHeadingText() { 12 | return 'Atom Build'; 13 | } 14 | 15 | static content() { 16 | this.div({ tabIndex: -1, class: 'build tool-panel native-key-bindings' }, () => { 17 | this.div({ class: 'heading', outlet: 'panelHeading' }, () => { 18 | this.div({ class: 'control-container opaque-hover' }, () => { 19 | this.button({ class: 'btn btn-default icon icon-zap', click: 'build', title: 'Build current project' }); 20 | this.button({ class: 'btn btn-default icon icon-trashcan', click: 'clearOutput' }); 21 | this.button({ class: 'btn btn-default icon icon-x', click: 'close' }); 22 | this.div({ class: 'title', outlet: 'title' }, () => { 23 | this.span({ class: 'build-timer', outlet: 'buildTimer' }, this.initialTimerText()); 24 | }); 25 | }); 26 | this.div({ class: 'icon heading-text', outlet: 'heading' }, this.initialHeadingText()); 27 | }); 28 | 29 | this.div({ class: 'output panel-body', outlet: 'output' }); 30 | this.div({ class: 'resizer', outlet: 'resizer' }); 31 | }); 32 | } 33 | 34 | constructor(...args) { 35 | super(...args); 36 | const Terminal = require('term.js'); 37 | this.starttime = new Date(); 38 | this.terminal = new Terminal({ 39 | cursorBlink: false, 40 | convertEol: true, 41 | useFocus: false, 42 | termName: 'xterm-256color', 43 | scrollback: atom.config.get('build.terminalScrollback') 44 | }); 45 | 46 | // On some systems, prependListern and prependOnceListener is expected to exist. Add them until terminal replacement is here. 47 | this.terminal.prependListener = (...a) => { 48 | this.terminal.addListener(...a); 49 | }; 50 | this.terminal.prependOnceListener = (...a) => { 51 | this.terminal.addOnceListener(...a); 52 | }; 53 | 54 | this.terminal.getContent = function () { 55 | return this.lines.reduce((m1, line) => { 56 | return m1 + line.reduce((m2, col) => m2 + col[1], '') + '\n'; 57 | }, ''); 58 | }; 59 | 60 | this.fontGeometry = { w: 15, h: 15 }; 61 | this.terminal.open(this.output[0]); 62 | this.destroyTerminal = ::(this.terminal).destroy; 63 | this.terminal.destroy = this.terminal.destroySoon = () => {}; // This terminal will be open forever and reset when necessary 64 | this.terminalEl = $(this.terminal.element); 65 | this.terminalEl[0].terminal = this.terminal; // For testing purposes 66 | 67 | this.resizeStarted = ::this.resizeStarted; 68 | this.resizeMoved = ::this.resizeMoved; 69 | this.resizeEnded = ::this.resizeEnded; 70 | 71 | atom.config.observe('build.panelVisibility', ::this.visibleFromConfig); 72 | atom.config.observe('build.panelOrientation', ::this.orientationFromConfig); 73 | atom.config.observe('build.hidePanelHeading', (hide) => { 74 | hide && this.panelHeading.hide() || this.panelHeading.show(); 75 | }); 76 | atom.config.observe('build.overrideThemeColors', (override) => { 77 | this.output.removeClass('override-theme'); 78 | override && this.output.addClass('override-theme'); 79 | }); 80 | atom.config.observe('editor.fontSize', ::this.fontSizeFromConfig); 81 | atom.config.observe('editor.fontFamily', ::this.fontFamilyFromConfig); 82 | atom.commands.add('atom-workspace', 'build:toggle-panel', ::this.toggle); 83 | } 84 | 85 | destroy() { 86 | this.destroyTerminal(); 87 | clearInterval(this.detectResizeInterval); 88 | } 89 | 90 | resizeStarted() { 91 | document.body.style['-webkit-user-select'] = 'none'; 92 | document.addEventListener('mousemove', this.resizeMoved); 93 | document.addEventListener('mouseup', this.resizeEnded); 94 | } 95 | 96 | resizeMoved(ev) { 97 | const { h } = this.fontGeometry; 98 | 99 | switch (atom.config.get('build.panelOrientation')) { 100 | case 'Bottom': { 101 | const delta = this.resizer.get(0).getBoundingClientRect().top - ev.y; 102 | if (Math.abs(delta) < (h * 5 / 6)) return; 103 | 104 | const nearestRowHeight = Math.round((this.terminalEl.height() + delta) / h) * h; 105 | const maxHeight = $('.item-views').height() + $('.build .output').height(); 106 | this.terminalEl.css('height', `${Math.min(maxHeight, nearestRowHeight)}px`); 107 | break; 108 | } 109 | 110 | case 'Top': { 111 | const delta = this.resizer.get(0).getBoundingClientRect().top - ev.y; 112 | if (Math.abs(delta) < (h * 5 / 6)) return; 113 | 114 | const nearestRowHeight = Math.round((this.terminalEl.height() - delta) / h) * h; 115 | const maxHeight = $('.item-views').height() + $('.build .output').height(); 116 | this.terminalEl.css('height', `${Math.min(maxHeight, nearestRowHeight)}px`); 117 | break; 118 | } 119 | 120 | case 'Left': { 121 | const delta = this.resizer.get(0).getBoundingClientRect().right - ev.x; 122 | this.css('width', `${this.width() - delta - this.resizer.outerWidth()}px`); 123 | break; 124 | } 125 | 126 | case 'Right': { 127 | const delta = this.resizer.get(0).getBoundingClientRect().left - ev.x; 128 | this.css('width', `${this.width() + delta}px`); 129 | break; 130 | } 131 | } 132 | 133 | this.resizeTerminal(); 134 | } 135 | 136 | resizeEnded() { 137 | document.body.style['-webkit-user-select'] = 'text'; 138 | document.removeEventListener('mousemove', this.resizeMoved); 139 | document.removeEventListener('mouseup', this.resizeEnded); 140 | } 141 | 142 | resizeToNearestRow() { 143 | if (-1 !== [ 'Top', 'Bottom' ].indexOf(atom.config.get('build.panelOrientation'))) { 144 | this.fixTerminalElHeight(); 145 | } 146 | this.resizeTerminal(); 147 | } 148 | 149 | getFontGeometry() { 150 | const o = $('
A
') 151 | .addClass('terminal') 152 | .addClass('terminal-test') 153 | .appendTo(this.output); 154 | const w = o[0].getBoundingClientRect().width; 155 | const h = o[0].getBoundingClientRect().height; 156 | o.remove(); 157 | return { w, h }; 158 | } 159 | 160 | resizeTerminal() { 161 | this.fontGeometry = this.getFontGeometry(); 162 | const { w, h } = this.fontGeometry; 163 | if (0 === w || 0 === h) { 164 | return; 165 | } 166 | 167 | const terminalWidth = Math.floor((this.terminalEl.width()) / w); 168 | const terminalHeight = Math.floor((this.terminalEl.height()) / h); 169 | 170 | this.terminal.resize(terminalWidth, terminalHeight); 171 | } 172 | 173 | getContent() { 174 | return this.terminal.getContent(); 175 | } 176 | 177 | attach(force = false) { 178 | if (!force) { 179 | switch (atom.config.get('build.panelVisibility')) { 180 | case 'Hidden': 181 | case 'Show on Error': 182 | return; 183 | } 184 | } 185 | 186 | if (this.panel) { 187 | this.panel.destroy(); 188 | } 189 | 190 | const addfn = { 191 | Top: atom.workspace.addTopPanel, 192 | Bottom: atom.workspace.addBottomPanel, 193 | Left: atom.workspace.addLeftPanel, 194 | Right: atom.workspace.addRightPanel 195 | }; 196 | const orientation = atom.config.get('build.panelOrientation') || 'Bottom'; 197 | this.panel = addfn[orientation].call(atom.workspace, { item: this }); 198 | this.fixTerminalElHeight(); 199 | this.resizeToNearestRow(); 200 | } 201 | 202 | fixTerminalElHeight() { 203 | const nearestRowHeight = $('.build .output').height(); 204 | this.terminalEl.css('height', `${nearestRowHeight}px`); 205 | } 206 | 207 | detach(force) { 208 | force = force || false; 209 | if (atom.views.getView(atom.workspace) && document.activeElement === this[0]) { 210 | atom.views.getView(atom.workspace).focus(); 211 | } 212 | if (this.panel && (force || 'Keep Visible' !== atom.config.get('build.panelVisibility'))) { 213 | this.panel.destroy(); 214 | this.panel = null; 215 | } 216 | } 217 | 218 | isAttached() { 219 | return !!this.panel; 220 | } 221 | 222 | visibleFromConfig(val) { 223 | switch (val) { 224 | case 'Toggle': 225 | case 'Show on Error': 226 | if (!this.terminalEl.hasClass('error')) { 227 | this.detach(); 228 | } 229 | return; 230 | } 231 | 232 | this.attach(); 233 | } 234 | 235 | orientationFromConfig(orientation) { 236 | const isVisible = this.isVisible(); 237 | this.detach(true); 238 | if (isVisible) { 239 | this.attach(); 240 | } 241 | 242 | this.resizer.get(0).removeEventListener('mousedown', this.resizeStarted); 243 | 244 | switch (orientation) { 245 | case 'Top': 246 | case 'Bottom': 247 | this.get(0).style.width = null; 248 | this.resizer.get(0).addEventListener('mousedown', this.resizeStarted); 249 | break; 250 | 251 | case 'Left': 252 | case 'Right': 253 | this.terminalEl.get(0).style.height = null; 254 | this.resizer.get(0).addEventListener('mousedown', this.resizeStarted); 255 | break; 256 | } 257 | 258 | this.resizeTerminal(); 259 | } 260 | 261 | fontSizeFromConfig(size) { 262 | this.css({ 'font-size': size }); 263 | this.resizeToNearestRow(); 264 | } 265 | 266 | fontFamilyFromConfig(family) { 267 | this.css({ 'font-family': family }); 268 | this.resizeToNearestRow(); 269 | } 270 | 271 | reset() { 272 | clearTimeout(this.titleTimer); 273 | this.buildTimer.text(BuildView.initialTimerText()); 274 | this.titleTimer = 0; 275 | this.terminal.reset(); 276 | 277 | this.panelHeading.removeClass('success error'); 278 | this.title.removeClass('success error'); 279 | 280 | this.detach(); 281 | } 282 | 283 | updateTitle() { 284 | this.buildTimer.text(((new Date() - this.starttime) / 1000).toFixed(1) + ' s'); 285 | this.titleTimer = setTimeout(this.updateTitle.bind(this), 100); 286 | } 287 | 288 | close() { 289 | this.detach(true); 290 | } 291 | 292 | toggle() { 293 | require('./google-analytics').sendEvent('view', 'panel toggled'); 294 | this.isAttached() ? this.detach(true) : this.attach(true); 295 | } 296 | 297 | clearOutput() { 298 | this.terminal.reset(); 299 | } 300 | 301 | build() { 302 | atom.commands.dispatch(atom.views.getView(atom.workspace), 'build:trigger'); 303 | } 304 | 305 | setHeading(heading) { 306 | this.heading.text(heading); 307 | } 308 | 309 | buildStarted() { 310 | this.starttime = new Date(); 311 | this.reset(); 312 | this.attach(); 313 | if (atom.config.get('build.stealFocus')) { 314 | this.focus(); 315 | } 316 | this.updateTitle(); 317 | } 318 | 319 | buildFinished(success) { 320 | if (!success && !this.isAttached()) { 321 | this.attach(atom.config.get('build.panelVisibility') === 'Show on Error'); 322 | } 323 | this.finalizeBuild(success); 324 | } 325 | 326 | buildAbortInitiated() { 327 | this.heading.addClass('icon-stop'); 328 | } 329 | 330 | buildAborted() { 331 | this.finalizeBuild(false); 332 | } 333 | 334 | finalizeBuild(success) { 335 | this.title.addClass(success ? 'success' : 'error'); 336 | this.panelHeading.addClass(success ? 'success' : 'error'); 337 | this.heading.removeClass('icon-stop'); 338 | clearTimeout(this.titleTimer); 339 | } 340 | 341 | scrollTo(text) { 342 | const content = this.getContent(); 343 | let endPos = -1; 344 | let curPos = text.length; 345 | // We need to decrease the size of `text` until we find a match. This is because 346 | // terminal will insert line breaks ('\r\n') when width of terminal is reached. 347 | // It may have been that the middle of a matched error is on a line break. 348 | while (-1 === endPos && curPos > 0) { 349 | endPos = content.indexOf(text.substring(0, curPos--)); 350 | } 351 | 352 | if (curPos === 0) { 353 | // No match - which is weird. Oh well - rather be defensive 354 | return; 355 | } 356 | 357 | const row = content.slice(0, endPos).split('\n').length; 358 | this.terminal.ydisp = 0; 359 | this.terminal.scrollDisp(row - 1); 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /lib/build.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | export default { 4 | config: require('./config'), 5 | 6 | activate() { 7 | if (!/^win/.test(process.platform)) { 8 | // Manually append /usr/local/bin as it may not be set on some systems, 9 | // and it's common to have node installed here. Keep it at end so it won't 10 | // accidentially override any other node installation 11 | 12 | // Note: This should probably be removed in a end-user friendly way... 13 | process.env.PATH += ':/usr/local/bin'; 14 | } 15 | 16 | require('atom-package-deps').install('build'); 17 | 18 | this.tools = [ require('./atom-build') ]; 19 | this.linter = null; 20 | 21 | this.setupTargetManager(); 22 | this.setupBuildView(); 23 | this.setupErrorMatcher(); 24 | 25 | atom.commands.add('atom-workspace', 'build:trigger', () => this.build('trigger')); 26 | atom.commands.add('atom-workspace', 'build:stop', () => this.stop()); 27 | atom.commands.add('atom-workspace', 'build:confirm', () => { 28 | require('./google-analytics').sendEvent('build', 'confirmed'); 29 | document.activeElement.click(); 30 | }); 31 | atom.commands.add('atom-workspace', 'build:no-confirm', () => { 32 | if (this.saveConfirmView) { 33 | require('./google-analytics').sendEvent('build', 'not confirmed'); 34 | this.saveConfirmView.cancel(); 35 | } 36 | }); 37 | 38 | atom.workspace.observeTextEditors((editor) => { 39 | editor.onDidSave(() => { 40 | if (atom.config.get('build.buildOnSave')) { 41 | this.build('save'); 42 | } 43 | }); 44 | }); 45 | 46 | atom.workspace.onDidChangeActivePaneItem(() => this.updateStatusBar()); 47 | atom.packages.onDidActivateInitialPackages(() => this.targetManager.refreshTargets()); 48 | }, 49 | 50 | setupTargetManager() { 51 | const TargetManager = require('./target-manager'); 52 | this.targetManager = new TargetManager(); 53 | this.targetManager.setTools(this.tools); 54 | this.targetManager.on('refresh-complete', () => { 55 | this.updateStatusBar(); 56 | }); 57 | this.targetManager.on('new-active-target', (path, target) => { 58 | this.updateStatusBar(); 59 | 60 | if (atom.config.get('build.selectTriggers')) { 61 | this.build('trigger'); 62 | } 63 | }); 64 | this.targetManager.on('trigger', atomCommandName => this.build('trigger', atomCommandName)); 65 | }, 66 | 67 | setupBuildView() { 68 | const BuildView = require('./build-view'); 69 | this.buildView = new BuildView(); 70 | }, 71 | 72 | setupErrorMatcher() { 73 | const ErrorMatcher = require('./error-matcher'); 74 | this.errorMatcher = new ErrorMatcher(); 75 | this.errorMatcher.on('error', (message) => { 76 | atom.notifications.addError('Error matching failed!', { detail: message }); 77 | }); 78 | this.errorMatcher.on('matched', (match) => { 79 | match[0] && this.buildView.scrollTo(match[0]); 80 | }); 81 | }, 82 | 83 | deactivate() { 84 | if (this.child) { 85 | this.child.removeAllListeners(); 86 | require('tree-kill')(this.child.pid, 'SIGKILL'); 87 | this.child = null; 88 | } 89 | 90 | this.statusBarView && this.statusBarView.destroy(); 91 | this.buildView && this.buildView.destroy(); 92 | this.saveConfirmView && this.saveConfirmView.destroy(); 93 | this.linter && this.linter.destroy(); 94 | this.targetManager.destroy(); 95 | 96 | clearTimeout(this.finishedTimer); 97 | }, 98 | 99 | updateStatusBar() { 100 | const path = require('./utils').activePath(); 101 | const activeTarget = this.targetManager.getActiveTarget(path); 102 | this.statusBarView && activeTarget && this.statusBarView.setTarget(activeTarget.name); 103 | }, 104 | 105 | startNewBuild(source, atomCommandName) { 106 | const BuildError = require('./build-error'); 107 | const path = require('./utils').activePath(); 108 | let buildTitle = ''; 109 | this.linter && this.linter.clear(); 110 | 111 | Promise.resolve(this.targetManager.getTargets(path)).then(targets => { 112 | if (!targets || 0 === targets.length) { 113 | throw new BuildError('No eligible build target.', 'No configuration to build this project exists.'); 114 | } 115 | 116 | let target = targets.find(t => t.atomCommandName === atomCommandName); 117 | if (!target) { 118 | target = this.targetManager.getActiveTarget(path); 119 | } 120 | require('./google-analytics').sendEvent('build', 'triggered'); 121 | 122 | if (!target.exec) { 123 | throw new BuildError('Invalid build file.', 'No executable command specified.'); 124 | } 125 | 126 | this.statusBarView && this.statusBarView.buildStarted(); 127 | this.busyProvider && this.busyProvider.add(`Build: ${target.name}`); 128 | this.buildView.buildStarted(); 129 | this.buildView.setHeading('Running preBuild...'); 130 | 131 | return Promise.resolve(target.preBuild ? target.preBuild() : null).then(() => target); 132 | }).then(target => { 133 | const replace = require('./utils').replace; 134 | const env = Object.assign({}, process.env, target.env); 135 | Object.keys(env).forEach(key => { 136 | env[key] = replace(env[key], target.env); 137 | }); 138 | 139 | const exec = replace(target.exec, target.env); 140 | const args = target.args.map(arg => replace(arg, target.env)); 141 | const cwd = replace(target.cwd, target.env); 142 | const isWin = process.platform === 'win32'; 143 | const shCmd = isWin ? 'cmd' : '/bin/sh'; 144 | const shCmdArg = isWin ? '/C' : '-c'; 145 | 146 | // Store this as we need to re-set it after postBuild 147 | buildTitle = [ (target.sh ? `${shCmd} ${shCmdArg} ${exec}` : exec ), ...args, '\n'].join(' '); 148 | 149 | this.buildView.setHeading(buildTitle); 150 | if (target.sh) { 151 | this.child = require('child_process').spawn( 152 | shCmd, 153 | [ shCmdArg, [ exec ].concat(args).join(' ')], 154 | { cwd: cwd, env: env, stdio: ['ignore', null, null] } 155 | ); 156 | } else { 157 | this.child = require('cross-spawn').spawn( 158 | exec, 159 | args, 160 | { cwd: cwd, env: env, stdio: ['ignore', null, null] } 161 | ); 162 | } 163 | 164 | let stdout = ''; 165 | let stderr = ''; 166 | this.child.stdout.setEncoding('utf8'); 167 | this.child.stderr.setEncoding('utf8'); 168 | this.child.stdout.on('data', d => (stdout += d)); 169 | this.child.stderr.on('data', d => (stderr += d)); 170 | this.child.stdout.pipe(this.buildView.terminal); 171 | this.child.stderr.pipe(this.buildView.terminal); 172 | this.child.killSignals = (target.killSignals || [ 'SIGINT', 'SIGTERM', 'SIGKILL' ]).slice(); 173 | 174 | this.child.on('error', (err) => { 175 | this.buildView.terminal.write((target.sh ? 'Unable to execute with shell: ' : 'Unable to execute: ') + exec + '\n'); 176 | 177 | if (/\s/.test(exec) && !target.sh) { 178 | this.buildView.terminal.write('`cmd` cannot contain space. Use `args` for arguments.\n'); 179 | } 180 | 181 | if ('ENOENT' === err.code) { 182 | this.buildView.terminal.write(`Make sure cmd:'${exec}' and cwd:'${cwd}' exists and have correct access permissions.\n`); 183 | this.buildView.terminal.write(`Binaries are found in these folders: ${process.env.PATH}\n`); 184 | } 185 | }); 186 | 187 | this.child.on('close', (exitCode) => { 188 | this.child = null; 189 | this.errorMatcher.set(target, cwd, stdout + stderr); 190 | 191 | let success = (0 === exitCode); 192 | if (atom.config.get('build.matchedErrorFailsBuild')) { 193 | success = success && !this.errorMatcher.getMatches().some(match => match.type && match.type.toLowerCase() === 'error'); 194 | } 195 | 196 | this.linter && this.linter.processMessages(this.errorMatcher.getMatches(), cwd); 197 | 198 | if (atom.config.get('build.beepWhenDone')) { 199 | atom.beep(); 200 | } 201 | 202 | this.buildView.setHeading('Running postBuild...'); 203 | return Promise.resolve(target.postBuild ? target.postBuild(success, stdout, stderr) : null).then(() => { 204 | this.buildView.setHeading(buildTitle); 205 | 206 | this.busyProvider && this.busyProvider.remove(`Build: ${target.name}`, success); 207 | this.buildView.buildFinished(success); 208 | this.statusBarView && this.statusBarView.setBuildSuccess(success); 209 | if (success) { 210 | require('./google-analytics').sendEvent('build', 'succeeded'); 211 | this.finishedTimer = setTimeout(() => { 212 | this.buildView.detach(); 213 | }, 1200); 214 | } else { 215 | if (atom.config.get('build.scrollOnError')) { 216 | this.errorMatcher.matchFirst(); 217 | } 218 | require('./google-analytics').sendEvent('build', 'failed'); 219 | } 220 | 221 | this.nextBuild && this.nextBuild(); 222 | this.nextBuild = null; 223 | }); 224 | }); 225 | }).catch((err) => { 226 | if (err instanceof BuildError) { 227 | if (source === 'save') { 228 | // If there is no eligible build tool, and cause of build was a save, stay quiet. 229 | return; 230 | } 231 | 232 | atom.notifications.addWarning(err.name, { detail: err.message, stack: err.stack }); 233 | } else { 234 | atom.notifications.addError('Failed to build.', { detail: err.message, stack: err.stack }); 235 | } 236 | }); 237 | }, 238 | 239 | sendNextSignal() { 240 | try { 241 | const signal = this.child.killSignals.shift(); 242 | require('tree-kill')(this.child.pid, signal); 243 | } catch (e) { 244 | /* Something may have happened to the child (e.g. terminated by itself). Ignore this. */ 245 | } 246 | }, 247 | 248 | abort(cb) { 249 | if (!this.child.killed) { 250 | this.buildView.buildAbortInitiated(); 251 | this.child.killed = true; 252 | this.child.on('exit', () => { 253 | this.child = null; 254 | cb && cb(); 255 | }); 256 | } 257 | 258 | this.sendNextSignal(); 259 | }, 260 | 261 | build(source, event) { 262 | clearTimeout(this.finishedTimer); 263 | 264 | this.doSaveConfirm(this.unsavedTextEditors(), () => { 265 | const nextBuild = this.startNewBuild.bind(this, source, event ? event.type : null); 266 | if (this.child) { 267 | this.nextBuild = nextBuild; 268 | return this.abort(); 269 | } 270 | return nextBuild(); 271 | }); 272 | }, 273 | 274 | doSaveConfirm(modifiedTextEditors, continuecb, cancelcb) { 275 | const saveAndContinue = (save) => { 276 | modifiedTextEditors.forEach((textEditor) => save && textEditor.save()); 277 | continuecb(); 278 | }; 279 | 280 | if (0 === modifiedTextEditors.length || atom.config.get('build.saveOnBuild')) { 281 | saveAndContinue(true); 282 | return; 283 | } 284 | 285 | if (this.saveConfirmView) { 286 | this.saveConfirmView.destroy(); 287 | } 288 | 289 | const SaveConfirmView = require('./save-confirm-view'); 290 | this.saveConfirmView = new SaveConfirmView(); 291 | this.saveConfirmView.show(saveAndContinue, cancelcb); 292 | }, 293 | 294 | unsavedTextEditors() { 295 | return atom.workspace.getTextEditors().filter((textEditor) => { 296 | return textEditor.isModified() && (undefined !== textEditor.getPath()); 297 | }); 298 | }, 299 | 300 | stop() { 301 | this.nextBuild = null; 302 | clearTimeout(this.finishedTimer); 303 | if (this.child) { 304 | this.abort(() => { 305 | this.buildView.buildAborted(); 306 | this.statusBarView && this.statusBarView.buildAborted(); 307 | }); 308 | } else { 309 | this.buildView.reset(); 310 | } 311 | }, 312 | 313 | consumeLinterRegistry(registry) { 314 | this.linter && this.linter.destroy(); 315 | const Linter = require('./linter-integration'); 316 | this.linter = new Linter(registry); 317 | }, 318 | 319 | consumeBuilder(builder) { 320 | if (Array.isArray(builder)) this.tools.push(...builder); else this.tools.push(builder); 321 | this.targetManager.setTools(this.tools); 322 | const Disposable = require('atom').Disposable; 323 | return new Disposable(() => { 324 | this.tools = this.tools.filter(Array.isArray(builder) ? tool => builder.indexOf(tool) === -1 : tool => tool !== builder); 325 | this.targetManager.setTools(this.tools); 326 | }); 327 | }, 328 | 329 | consumeStatusBar(statusBar) { 330 | const StatusBarView = require('./status-bar-view'); 331 | this.statusBarView = new StatusBarView(statusBar); 332 | this.statusBarView.onClick(() => this.targetManager.selectActiveTarget()); 333 | this.statusBarView.attach(); 334 | }, 335 | 336 | consumeBusySignal(registry) { 337 | this.busyProvider = registry.create(); 338 | this.targetManager.setBusyProvider(this.busyProvider); 339 | } 340 | }; 341 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | export default { 4 | panelVisibility: { 5 | title: 'Panel Visibility', 6 | description: 'Set when the build panel should be visible.', 7 | type: 'string', 8 | default: 'Toggle', 9 | enum: [ 'Toggle', 'Keep Visible', 'Show on Error', 'Hidden' ], 10 | order: 1 11 | }, 12 | hidePanelHeading: { 13 | title: 'Hide panel heading', 14 | description: 'Set whether to hide the build command and control buttons in the build panel', 15 | type: 'boolean', 16 | default: false, 17 | order: 2 18 | }, 19 | buildOnSave: { 20 | title: 'Automatically build on save', 21 | description: 'Automatically build your project each time an editor is saved.', 22 | type: 'boolean', 23 | default: false, 24 | order: 3 25 | }, 26 | saveOnBuild: { 27 | title: 'Automatically save on build', 28 | description: 'Automatically save all edited files when triggering a build.', 29 | type: 'boolean', 30 | default: false, 31 | order: 4 32 | }, 33 | matchedErrorFailsBuild: { 34 | title: 'Any matched error will fail the build', 35 | description: 'Even if the build has a return code of zero it is marked as "failed" if any error is being matched in the output.', 36 | type: 'boolean', 37 | default: true, 38 | order: 5 39 | }, 40 | scrollOnError: { 41 | title: 'Automatically scroll on build error', 42 | description: 'Automatically scroll to first matched error when a build failed.', 43 | type: 'boolean', 44 | default: false, 45 | order: 6 46 | }, 47 | stealFocus: { 48 | title: 'Steal Focus', 49 | description: 'Steal focus when opening build panel.', 50 | type: 'boolean', 51 | default: true, 52 | order: 7 53 | }, 54 | overrideThemeColors: { 55 | title: 'Override Theme Colors', 56 | description: 'Override theme background- and text color inside the terminal', 57 | type: 'boolean', 58 | default: true, 59 | order: 8 60 | }, 61 | selectTriggers: { 62 | title: 'Selecting new target triggers the build', 63 | description: 'When selecting a new target (through status-bar, cmd-alt-t, etc), the newly selected target will be triggered.', 64 | type: 'boolean', 65 | default: true, 66 | order: 9 67 | }, 68 | refreshOnShowTargetList: { 69 | title: 'Refresh targets when the target list is shown', 70 | description: 'When opening the targets menu, the targets will be refreshed.', 71 | type: 'boolean', 72 | default: false, 73 | order: 10 74 | }, 75 | notificationOnRefresh: { 76 | title: 'Show notification when targets are refreshed', 77 | description: 'When targets are refreshed a notification with information about the number of targets will be displayed.', 78 | type: 'boolean', 79 | default: false, 80 | order: 11 81 | }, 82 | beepWhenDone: { 83 | title: 'Beep when the build completes', 84 | description: 'Make a "beep" notification sound when the build is complete - in success or failure.', 85 | type: 'boolean', 86 | default: false, 87 | order: 12 88 | }, 89 | panelOrientation: { 90 | title: 'Panel Orientation', 91 | description: 'Where to attach the build panel', 92 | type: 'string', 93 | default: 'Bottom', 94 | enum: [ 'Bottom', 'Top', 'Left', 'Right' ], 95 | order: 13 96 | }, 97 | statusBar: { 98 | title: 'Status Bar', 99 | description: 'Where to place the status bar. Set to `Disable` to disable status bar display.', 100 | type: 'string', 101 | default: 'Left', 102 | enum: [ 'Left', 'Right', 'Disable' ], 103 | order: 14 104 | }, 105 | statusBarPriority: { 106 | title: 'Priority on Status Bar', 107 | description: 'Lower priority tiles are placed further to the left/right, depends on where you choose to place Status Bar.', 108 | type: 'number', 109 | default: -1000, 110 | order: 15 111 | }, 112 | terminalScrollback: { 113 | title: 'Terminal Scrollback Size', 114 | description: 'Max number of lines of build log kept in the terminal', 115 | type: 'number', 116 | default: 1000, 117 | order: 16 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /lib/error-matcher.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { EventEmitter } from 'events'; 4 | 5 | export default class ErrorMatcher extends EventEmitter { 6 | 7 | constructor() { 8 | super(); 9 | this.regex = null; 10 | this.cwd = null; 11 | this.stdout = null; 12 | this.stderr = null; 13 | this.currentMatch = []; 14 | this.firstMatchId = null; 15 | 16 | atom.commands.add('atom-workspace', 'build:error-match', ::this.match); 17 | atom.commands.add('atom-workspace', 'build:error-match-first', ::this.matchFirst); 18 | } 19 | 20 | _gotoNext() { 21 | if (0 === this.currentMatch.length) { 22 | return; 23 | } 24 | 25 | this.goto(this.currentMatch[0].id); 26 | } 27 | 28 | goto(id) { 29 | const match = this.currentMatch.find(m => m.id === id); 30 | if (!match) { 31 | this.emit('error', 'Can\'t find match with id ' + id); 32 | return; 33 | } 34 | 35 | // rotate to next match 36 | while (this.currentMatch[0] !== match) { 37 | this.currentMatch.push(this.currentMatch.shift()); 38 | } 39 | this.currentMatch.push(this.currentMatch.shift()); 40 | 41 | let file = match.file; 42 | if (!file) { 43 | this.emit('error', 'Did not match any file. Don\'t know what to open.'); 44 | return; 45 | } 46 | 47 | const path = require('path'); 48 | if (!path.isAbsolute(file)) { 49 | file = this.cwd + path.sep + file; 50 | } 51 | 52 | const row = match.line ? match.line - 1 : 0; /* Because atom is zero-based */ 53 | const col = match.col ? match.col - 1 : 0; /* Because atom is zero-based */ 54 | 55 | require('fs').exists(file, (exists) => { 56 | if (!exists) { 57 | this.emit('error', 'Matched file does not exist: ' + file); 58 | return; 59 | } 60 | atom.workspace.open(file, { 61 | initialLine: row, 62 | initialColumn: col, 63 | searchAllPanes: true 64 | }); 65 | this.emit('matched', match); 66 | }); 67 | } 68 | 69 | _parse() { 70 | this.currentMatch = []; 71 | 72 | // first run all functional matches 73 | this.functions && this.functions.forEach((f, functionIndex) => { 74 | this.currentMatch = this.currentMatch.concat(f(this.output).map((match, matchIndex) => { 75 | match.id = 'error-match-function-' + functionIndex + '-' + matchIndex; 76 | match.type = match.type || 'Error'; 77 | return match; 78 | })); 79 | }); 80 | // then for all match kinds 81 | Object.keys(this.regex).forEach(kind => { 82 | // run all matches 83 | this.regex[kind] && this.regex[kind].forEach((regex, i) => { 84 | regex && require('xregexp').forEach(this.output, regex, (match, matchIndex) => { 85 | match.id = 'error-match-' + i + '-' + matchIndex; 86 | match.type = kind; 87 | this.currentMatch.push(match); 88 | }); 89 | }); 90 | }); 91 | 92 | this.currentMatch.sort((a, b) => a.index - b.index); 93 | 94 | this.firstMatchId = (this.currentMatch.length > 0) ? this.currentMatch[0].id : null; 95 | } 96 | 97 | _prepareRegex(regex) { 98 | regex = regex || []; 99 | regex = (regex instanceof Array) ? regex : [ regex ]; 100 | 101 | return regex.map(r => { 102 | try { 103 | const XRegExp = require('xregexp'); 104 | return XRegExp(r); 105 | } catch (err) { 106 | this.emit('error', 'Error parsing regex. ' + err.message); 107 | return null; 108 | } 109 | }); 110 | } 111 | 112 | set(target, cwd, output) { 113 | if (target.functionMatch) { 114 | this.functions = ((target.functionMatch instanceof Array) ? target.functionMatch : [ target.functionMatch ]).filter(f => { 115 | if (typeof f !== 'function') { 116 | this.emit('error', 'found functionMatch that is no function: ' + typeof f); 117 | return false; 118 | } 119 | return true; 120 | }); 121 | } 122 | this.regex = { 123 | Error: this._prepareRegex(target.errorMatch), 124 | Warning: this._prepareRegex(target.warningMatch) 125 | }; 126 | 127 | this.cwd = cwd; 128 | this.output = output; 129 | this.currentMatch = []; 130 | 131 | this._parse(); 132 | } 133 | 134 | match() { 135 | require('./google-analytics').sendEvent('errorMatch', 'match'); 136 | 137 | this._gotoNext(); 138 | } 139 | 140 | matchFirst() { 141 | require('./google-analytics').sendEvent('errorMatch', 'first'); 142 | 143 | if (this.firstMatchId) { 144 | this.goto(this.firstMatchId); 145 | } 146 | } 147 | 148 | hasMatch() { 149 | return 0 !== this.currentMatch.length; 150 | } 151 | 152 | getMatches() { 153 | return this.currentMatch; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/google-analytics.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | function uuid() { 4 | function s4() { 5 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 6 | } 7 | return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 8 | } 9 | 10 | export default class GoogleAnalytics { 11 | static getCid(cb) { 12 | if (this.cid) { 13 | cb(this.cid); 14 | return; 15 | } 16 | 17 | require('getmac').getMac((error, macAddress) => { 18 | return error ? 19 | cb(this.cid = uuid()) : 20 | cb(this.cid = require('crypto').createHash('sha1').update(macAddress, 'utf8').digest('hex')); 21 | }); 22 | } 23 | 24 | static sendEvent(category, action, label, value) { 25 | const params = { 26 | t: 'event', 27 | ec: category, 28 | ea: action 29 | }; 30 | if (label) { 31 | params.el = label; 32 | } 33 | if (value) { 34 | params.ev = value; 35 | } 36 | 37 | this.send(params); 38 | } 39 | 40 | static send(params) { 41 | if (!atom.packages.getActivePackage('metrics')) { 42 | // If the metrics package is disabled, then user has opted out. 43 | return; 44 | } 45 | 46 | GoogleAnalytics.getCid((cid) => { 47 | Object.assign(params, { cid: cid }, GoogleAnalytics.defaultParams()); 48 | this.request('https://www.google-analytics.com/collect?' + require('querystring').stringify(params)); 49 | }); 50 | } 51 | 52 | static request(url) { 53 | if (!navigator.onLine) { 54 | return; 55 | } 56 | this.post(url); 57 | } 58 | 59 | static post(url) { 60 | const xhr = new XMLHttpRequest(); 61 | xhr.open('POST', url); 62 | xhr.send(null); 63 | } 64 | 65 | static defaultParams() { 66 | // https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters 67 | return { 68 | v: 1, 69 | tid: 'UA-47615700-5' 70 | }; 71 | } 72 | } 73 | 74 | atom.packages.onDidActivatePackage((pkg) => { 75 | if ('metrics' === pkg.name) { 76 | const buildPackage = atom.packages.getLoadedPackage('build'); 77 | require('./google-analytics').sendEvent('core', 'activated', buildPackage.metadata.version); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /lib/linter-integration.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | class Linter { 4 | constructor(registry) { 5 | this.linter = registry.register({ name: 'Build' }); 6 | } 7 | destroy() { 8 | this.linter.dispose(); 9 | } 10 | clear() { 11 | this.linter.deleteMessages(); 12 | } 13 | processMessages(messages, cwd) { 14 | function extractRange(json) { 15 | return [ 16 | [ (json.line || 1) - 1, (json.col || 1) - 1 ], 17 | [ (json.line_end || json.line || 1) - 1, (json.col_end || json.col || 1) - 1 ] 18 | ]; 19 | } 20 | function normalizePath(p) { 21 | return require('path').isAbsolute(p) ? p : require('path').join(cwd, p); 22 | } 23 | function typeToSeverity(type) { 24 | switch (type && type.toLowerCase()) { 25 | case 'err': 26 | case 'error': return 'error'; 27 | case 'warn': 28 | case 'warning': return 'warning'; 29 | default: return null; 30 | } 31 | } 32 | this.linter.setMessages(messages.map(match => ({ 33 | type: match.type || 'Error', 34 | text: !match.message && !match.html_message ? 'Error from build' : match.message, 35 | html: match.message ? undefined : match.html_message, 36 | filePath: normalizePath(match.file), 37 | severity: typeToSeverity(match.type), 38 | range: extractRange(match), 39 | trace: match.trace && match.trace.map(trace => ({ 40 | type: trace.type || 'Trace', 41 | text: !trace.message && !trace.html_message ? 'Trace in build' : trace.message, 42 | html: trace.message ? undefined : trace.html_message, 43 | filePath: trace.file && normalizePath(trace.file), 44 | severity: typeToSeverity(trace.type) || 'info', 45 | range: extractRange(trace) 46 | })) 47 | }))); 48 | } 49 | } 50 | 51 | export default Linter; 52 | -------------------------------------------------------------------------------- /lib/save-confirm-view.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { View } from 'atom-space-pen-views'; 4 | 5 | export default class SaveConfirmView extends View { 6 | static content() { 7 | this.div({ class: 'build-confirm overlay from-top' }, () => { 8 | this.h3('You have unsaved changes'); 9 | this.div({ class: 'btn-container pull-right' }, () => { 10 | this.button({ class: 'btn btn-success', outlet: 'saveBuildButton', title: 'Save and Build', click: 'saveAndConfirm' }, 'Save and build'); 11 | this.button({ class: 'btn btn-info', title: 'Build Without Saving', click: 'confirmWithoutSave' }, 'Build Without Saving'); 12 | }); 13 | this.div({ class: 'btn-container pull-left' }, () => { 14 | this.button({ class: 'btn btn-info', title: 'Cancel', click: 'cancel' }, 'Cancel'); 15 | }); 16 | }); 17 | } 18 | 19 | destroy() { 20 | this.confirmcb = undefined; 21 | this.cancelcb = undefined; 22 | if (this.panel) { 23 | this.panel.destroy(); 24 | this.panel = null; 25 | } 26 | } 27 | 28 | show(confirmcb, cancelcb) { 29 | this.confirmcb = confirmcb; 30 | this.cancelcb = cancelcb; 31 | 32 | this.panel = atom.workspace.addTopPanel({ 33 | item: this 34 | }); 35 | this.saveBuildButton.focus(); 36 | } 37 | 38 | cancel() { 39 | this.destroy(); 40 | if (this.cancelcb) { 41 | this.cancelcb(); 42 | } 43 | } 44 | 45 | saveAndConfirm() { 46 | if (this.confirmcb) { 47 | this.confirmcb(true); 48 | } 49 | this.destroy(); 50 | } 51 | 52 | confirmWithoutSave() { 53 | if (this.confirmcb) { 54 | this.confirmcb(false); 55 | } 56 | this.destroy(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/status-bar-view.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { View } from 'atom-space-pen-views'; 4 | 5 | export default class StatusBarView extends View { 6 | constructor(statusBar, ...args) { 7 | super(...args); 8 | this.statusBar = statusBar; 9 | atom.config.observe('build.statusBar', () => this.attach()); 10 | atom.config.observe('build.statusBarPriority', () => this.attach()); 11 | } 12 | 13 | attach() { 14 | this.destroy(); 15 | 16 | const orientation = atom.config.get('build.statusBar'); 17 | if ('Disable' === orientation) { 18 | return; 19 | } 20 | 21 | this.statusBarTile = this.statusBar[`add${orientation}Tile`]({ 22 | item: this, 23 | priority: atom.config.get('build.statusBarPriority') 24 | }); 25 | 26 | this.tooltip = atom.tooltips.add(this, { 27 | title: () => this.tooltipMessage() 28 | }); 29 | } 30 | 31 | destroy() { 32 | if (this.statusBarTile) { 33 | this.statusBarTile.destroy(); 34 | this.statusBarTile = null; 35 | } 36 | 37 | if (this.tooltip) { 38 | this.tooltip.dispose(); 39 | this.tooltip = null; 40 | } 41 | } 42 | 43 | static content() { 44 | this.div({ id: 'build-status-bar', class: 'inline-block' }, () => { 45 | this.a({ click: 'clicked', outlet: 'message'}); 46 | }); 47 | } 48 | 49 | tooltipMessage() { 50 | return `Current build target is '${this.element.textContent}'`; 51 | } 52 | 53 | setClasses(classes) { 54 | this.removeClass('status-unknown status-success status-error'); 55 | this.addClass(classes); 56 | } 57 | 58 | setTarget(t) { 59 | if (this.target === t) { 60 | return; 61 | } 62 | 63 | this.target = t; 64 | this.message.text(t || ''); 65 | this.setClasses(); 66 | } 67 | 68 | buildAborted() { 69 | this.setBuildSuccess(false); 70 | } 71 | 72 | setBuildSuccess(success) { 73 | this.setClasses(success ? 'status-success' : 'status-error'); 74 | } 75 | 76 | buildStarted() { 77 | this.setClasses(); 78 | } 79 | 80 | onClick(cb) { 81 | this.onClick = cb; 82 | } 83 | 84 | clicked() { 85 | this.onClick && this.onClick(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/target-manager.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import EventEmitter from 'events'; 4 | 5 | class TargetManager extends EventEmitter { 6 | constructor() { 7 | super(); 8 | 9 | let projectPaths = atom.project.getPaths(); 10 | 11 | this.pathTargets = projectPaths.map(path => this._defaultPathTarget(path)); 12 | 13 | atom.project.onDidChangePaths(newProjectPaths => { 14 | const addedPaths = newProjectPaths.filter(el => projectPaths.indexOf(el) === -1); 15 | const removedPaths = projectPaths.filter(el => newProjectPaths.indexOf(el) === -1); 16 | addedPaths.forEach(path => this.pathTargets.push(this._defaultPathTarget(path))); 17 | this.pathTargets = this.pathTargets.filter(pt => -1 === removedPaths.indexOf(pt.path)); 18 | this.refreshTargets(addedPaths); 19 | projectPaths = newProjectPaths; 20 | }); 21 | 22 | atom.commands.add('atom-workspace', 'build:refresh-targets', () => this.refreshTargets()); 23 | atom.commands.add('atom-workspace', 'build:select-active-target', () => this.selectActiveTarget()); 24 | } 25 | 26 | setBusyProvider(busyProvider) { 27 | this.busyProvider = busyProvider; 28 | } 29 | 30 | _defaultPathTarget(path) { 31 | const CompositeDisposable = require('atom').CompositeDisposable; 32 | return { 33 | path: path, 34 | loading: false, 35 | targets: [], 36 | instancedTools: [], 37 | activeTarget: null, 38 | tools: [], 39 | subscriptions: new CompositeDisposable() 40 | }; 41 | } 42 | 43 | destroy() { 44 | this.pathTargets.forEach(pathTarget => pathTarget.tools.map(tool => { 45 | tool.removeAllListeners && tool.removeAllListeners('refresh'); 46 | tool.destructor && tool.destructor(); 47 | })); 48 | } 49 | 50 | setTools(tools) { 51 | this.tools = tools || []; 52 | } 53 | 54 | refreshTargets(refreshPaths) { 55 | refreshPaths = refreshPaths || atom.project.getPaths(); 56 | 57 | this.busyProvider && this.busyProvider.add(`Refreshing targets for ${refreshPaths.join(',')}`); 58 | const pathPromises = refreshPaths.map((path) => { 59 | const pathTarget = this.pathTargets.find(pt => pt.path === path); 60 | pathTarget.loading = true; 61 | 62 | pathTarget.instancedTools = pathTarget.instancedTools 63 | .map(t => t.removeAllListeners && t.removeAllListeners('refresh')) 64 | .filter(() => false); // Just empty the array 65 | 66 | const settingsPromise = this.tools 67 | .map(Tool => new Tool(path)) 68 | .filter(tool => tool.isEligible()) 69 | .map(tool => { 70 | pathTarget.instancedTools.push(tool); 71 | require('./google-analytics').sendEvent('build', 'tool eligible', tool.getNiceName()); 72 | 73 | tool.on && tool.on('refresh', this.refreshTargets.bind(this, [ path ])); 74 | return Promise.resolve() 75 | .then(() => tool.settings()) 76 | .catch(err => { 77 | if (err instanceof SyntaxError) { 78 | atom.notifications.addError('Invalid build file.', { 79 | detail: 'You have a syntax error in your build file: ' + err.message, 80 | dismissable: true 81 | }); 82 | } else { 83 | const toolName = tool.getNiceName(); 84 | atom.notifications.addError('Ooops. Something went wrong' + (toolName ? ' in the ' + toolName + ' build provider' : '') + '.', { 85 | detail: err.message, 86 | stack: err.stack, 87 | dismissable: true 88 | }); 89 | } 90 | }); 91 | }); 92 | 93 | const CompositeDisposable = require('atom').CompositeDisposable; 94 | return Promise.all(settingsPromise).then((settings) => { 95 | settings = require('./utils').uniquifySettings([].concat.apply([], settings) 96 | .filter(Boolean) 97 | .map(setting => require('./utils').getDefaultSettings(path, setting))); 98 | 99 | if (null === pathTarget.activeTarget || !settings.find(s => s.name === pathTarget.activeTarget)) { 100 | /* Active target has been removed or not set. Set it to the highest prio target */ 101 | pathTarget.activeTarget = settings[0] ? settings[0].name : undefined; 102 | } 103 | 104 | // CompositeDisposable cannot be reused, so we must create a new instance on every refresh 105 | pathTarget.subscriptions.dispose(); 106 | pathTarget.subscriptions = new CompositeDisposable(); 107 | 108 | settings.forEach((setting, index) => { 109 | if (setting.keymap && !setting.atomCommandName) { 110 | setting.atomCommandName = `build:trigger:${setting.name}`; 111 | } 112 | 113 | if (setting.atomCommandName) { 114 | pathTarget.subscriptions.add(atom.commands.add('atom-workspace', setting.atomCommandName, atomCommandName => this.emit('trigger', atomCommandName))); 115 | } 116 | 117 | if (setting.keymap) { 118 | require('./google-analytics').sendEvent('keymap', 'registered', setting.keymap); 119 | const keymapSpec = { 'atom-workspace, atom-text-editor': {} }; 120 | keymapSpec['atom-workspace, atom-text-editor'][setting.keymap] = setting.atomCommandName; 121 | pathTarget.subscriptions.add(atom.keymaps.add(setting.name, keymapSpec)); 122 | } 123 | }); 124 | 125 | pathTarget.targets = settings; 126 | pathTarget.loading = false; 127 | 128 | return pathTarget; 129 | }).catch(err => { 130 | atom.notifications.addError('Ooops. Something went wrong.', { 131 | detail: err.message, 132 | stack: err.stack, 133 | dismissable: true 134 | }); 135 | }); 136 | }); 137 | 138 | return Promise.all(pathPromises).then(pathTargets => { 139 | this.fillTargets(require('./utils').activePath(), false); 140 | this.emit('refresh-complete'); 141 | this.busyProvider && this.busyProvider.remove(`Refreshing targets for ${refreshPaths.join(',')}`); 142 | 143 | if (pathTargets.length === 0) { 144 | return; 145 | } 146 | 147 | if (atom.config.get('build.notificationOnRefresh')) { 148 | const rows = refreshPaths.map(path => { 149 | const pathTarget = this.pathTargets.find(pt => pt.path === path); 150 | if (!pathTarget) { 151 | return `Targets ${path} no longer exists. Is build deactivated?`; 152 | } 153 | return `${pathTarget.targets.length} targets at: ${path}`; 154 | }); 155 | atom.notifications.addInfo('Build targets parsed.', { 156 | detail: rows.join('\n') 157 | }); 158 | } 159 | }).catch(err => { 160 | atom.notifications.addError('Ooops. Something went wrong.', { 161 | detail: err.message, 162 | stack: err.stack, 163 | dismissable: true 164 | }); 165 | }); 166 | } 167 | 168 | fillTargets(path, refreshOnEmpty = true) { 169 | if (!this.targetsView) { 170 | return; 171 | } 172 | 173 | const activeTarget = this.getActiveTarget(path); 174 | activeTarget && this.targetsView.setActiveTarget(activeTarget.name); 175 | 176 | this.getTargets(path, refreshOnEmpty) 177 | .then(targets => targets.map(t => t.name)) 178 | .then(targetNames => this.targetsView && this.targetsView.setItems(targetNames)); 179 | } 180 | 181 | selectActiveTarget() { 182 | if (atom.config.get('build.refreshOnShowTargetList')) { 183 | this.refreshTargets(); 184 | } 185 | 186 | const path = require('./utils').activePath(); 187 | if (!path) { 188 | atom.notifications.addWarning('Unable to build.', { 189 | detail: 'Open file is not part of any open project in Atom' 190 | }); 191 | return; 192 | } 193 | 194 | const TargetsView = require('./targets-view'); 195 | this.targetsView = new TargetsView(); 196 | 197 | if (this.isLoading(path)) { 198 | this.targetsView.setLoading('Loading project build targets\u2026'); 199 | } else { 200 | this.fillTargets(path); 201 | } 202 | 203 | this.targetsView.awaitSelection().then(newTarget => { 204 | this.setActiveTarget(path, newTarget); 205 | 206 | this.targetsView = null; 207 | }).catch((err) => { 208 | this.targetsView.setError(err.message); 209 | this.targetsView = null; 210 | }); 211 | } 212 | 213 | getTargets(path, refreshOnEmpty = true) { 214 | const pathTarget = this.pathTargets.find(pt => pt.path === path); 215 | if (!pathTarget) { 216 | return Promise.resolve([]); 217 | } 218 | 219 | if (refreshOnEmpty && pathTarget.targets.length === 0) { 220 | return this.refreshTargets([ pathTarget.path ]).then(() => pathTarget.targets); 221 | } 222 | return Promise.resolve(pathTarget.targets); 223 | } 224 | 225 | getActiveTarget(path) { 226 | const pathTarget = this.pathTargets.find(pt => pt.path === path); 227 | if (!pathTarget) { 228 | return null; 229 | } 230 | return pathTarget.targets.find(target => target.name === pathTarget.activeTarget); 231 | } 232 | 233 | setActiveTarget(path, targetName) { 234 | this.pathTargets.find(pt => pt.path === path).activeTarget = targetName; 235 | this.emit('new-active-target', path, this.getActiveTarget(path)); 236 | } 237 | 238 | isLoading(path) { 239 | return this.pathTargets.find(pt => pt.path === path).loading; 240 | } 241 | } 242 | 243 | export default TargetManager; 244 | -------------------------------------------------------------------------------- /lib/targets-view.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { SelectListView } from 'atom-space-pen-views'; 4 | 5 | export default class TargetsView extends SelectListView { 6 | 7 | constructor() { 8 | super(...arguments); 9 | this.show(); 10 | } 11 | 12 | initialize() { 13 | super.initialize(...arguments); 14 | this.addClass('build-target'); 15 | this.list.addClass('mark-active'); 16 | } 17 | 18 | show() { 19 | this.panel = atom.workspace.addModalPanel({ item: this }); 20 | this.panel.show(); 21 | this.focusFilterEditor(); 22 | } 23 | 24 | hide() { 25 | this.panel.hide(); 26 | } 27 | 28 | setItems() { 29 | super.setItems(...arguments); 30 | 31 | const activeItemView = this.find('.active'); 32 | if (0 < activeItemView.length) { 33 | this.selectItemView(activeItemView); 34 | this.scrollToItemView(activeItemView); 35 | } 36 | } 37 | 38 | setActiveTarget(target) { 39 | this.activeTarget = target; 40 | } 41 | 42 | viewForItem(targetName) { 43 | const activeTarget = this.activeTarget; 44 | return TargetsView.render(function () { 45 | const activeClass = (targetName === activeTarget ? 'active' : ''); 46 | this.li({ class: activeClass + ' build-target' }, targetName); 47 | }); 48 | } 49 | 50 | getEmptyMessage(itemCount) { 51 | return (0 === itemCount) ? 'No targets found.' : 'No matches'; 52 | } 53 | 54 | awaitSelection() { 55 | return new Promise((resolve, reject) => { 56 | this.resolveFunction = resolve; 57 | }); 58 | } 59 | 60 | confirmed(target) { 61 | if (this.resolveFunction) { 62 | this.resolveFunction(target); 63 | this.resolveFunction = null; 64 | } 65 | this.hide(); 66 | } 67 | 68 | cancelled() { 69 | this.hide(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | const uniquifySettings = (settings) => { 7 | const genName = (name, index) => `${name} - ${index}`; 8 | const newSettings = []; 9 | settings.forEach(setting => { 10 | let i = 0; 11 | let testName = setting.name; 12 | while (newSettings.find(ns => ns.name === testName)) { // eslint-disable-line no-loop-func 13 | testName = genName(setting.name, ++i); 14 | } 15 | newSettings.push({ ...setting, name: testName }); 16 | }); 17 | return newSettings; 18 | }; 19 | 20 | const activePath = () => { 21 | const textEditor = atom.workspace.getActiveTextEditor(); 22 | if (!textEditor || !textEditor.getPath()) { 23 | /* default to building the first one if no editor is active */ 24 | if (0 === atom.project.getPaths().length) { 25 | return false; 26 | } 27 | 28 | return atom.project.getPaths()[0]; 29 | } 30 | 31 | /* otherwise, build the one in the root of the active editor */ 32 | return atom.project.getPaths().sort((a, b) => (b.length - a.length)).find(p => { 33 | try { 34 | const realpath = fs.realpathSync(p); 35 | return fs.realpathSync(textEditor.getPath()).substr(0, realpath.length) === realpath; 36 | } catch (err) { 37 | /* Path no longer available. Possible network volume has gone down */ 38 | return false; 39 | } 40 | }); 41 | }; 42 | 43 | const getDefaultSettings = (cwd, setting) => { 44 | return Object.assign({}, setting, { 45 | env: setting.env || {}, 46 | args: setting.args || [], 47 | cwd: setting.cwd || cwd, 48 | sh: (undefined === setting.sh) ? true : setting.sh, 49 | errorMatch: setting.errorMatch || '' 50 | }); 51 | }; 52 | 53 | const replace = (value = '', targetEnv) => { 54 | if (!(typeof value === 'string')) { 55 | return value; 56 | } 57 | 58 | const env = Object.assign({}, process.env, targetEnv); 59 | value = value.replace(/\$(\w+)/g, function (match, name) { 60 | return name in env ? env[name] : match; 61 | }); 62 | 63 | const editor = atom.workspace.getActiveTextEditor(); 64 | 65 | const projectPaths = atom.project.getPaths().map(projectPath => { 66 | try { 67 | return fs.realpathSync(projectPath); 68 | } catch (e) { /* Do nothing. */ } 69 | return null; 70 | }); 71 | 72 | let projectPath = projectPaths[0]; 73 | if (editor && (undefined !== editor.getPath())) { 74 | const activeFile = fs.realpathSync(editor.getPath()); 75 | const activeFilePath = path.dirname(activeFile); 76 | projectPath = projectPaths.find(p => activeFilePath && activeFilePath.startsWith(p)); 77 | value = value.replace(/{FILE_ACTIVE}/g, activeFile); 78 | value = value.replace(/{FILE_ACTIVE_PATH}/g, activeFilePath); 79 | value = value.replace(/{FILE_ACTIVE_NAME}/g, path.basename(activeFile)); 80 | value = value.replace(/{FILE_ACTIVE_NAME_BASE}/g, path.basename(activeFile, path.extname(activeFile))); 81 | value = value.replace(/{SELECTION}/g, editor.getSelectedText()); 82 | const cursorScreenPosition = editor.getCursorScreenPosition(); 83 | value = value.replace(/{FILE_ACTIVE_CURSOR_ROW}/g, cursorScreenPosition.row + 1); 84 | value = value.replace(/{FILE_ACTIVE_CURSOR_COLUMN}/g, cursorScreenPosition.column + 1); 85 | } 86 | value = value.replace(/{PROJECT_PATH}/g, projectPath); 87 | if (atom.project.getRepositories[0]) { 88 | value = value.replace(/{REPO_BRANCH_SHORT}/g, atom.project.getRepositories()[0].getShortHead()); 89 | } 90 | 91 | return value; 92 | }; 93 | 94 | export { uniquifySettings, activePath, getDefaultSettings, replace }; 95 | -------------------------------------------------------------------------------- /menus/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ { 3 | "label": "Packages", 4 | "submenu": [ { 5 | "label": "Build", 6 | "submenu": [ { 7 | "label": "Build project", 8 | "command": "build:trigger" 9 | }, 10 | { 11 | "label": "Terminate build", 12 | "command": "build:stop" 13 | }, 14 | { 15 | "label": "Select active target", 16 | "command": "build:select-active-target" 17 | }, 18 | { 19 | "label": "Refresh targets", 20 | "command": "build:refresh-targets" 21 | }, 22 | { 23 | "label": "Toggle panel", 24 | "command": "build:toggle-panel" 25 | } ] 26 | } ] 27 | } ], 28 | 29 | "context-menu": {} 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build", 3 | "main": "./lib/build", 4 | "version": "0.70.0", 5 | "description": "Build your current project, directly from Atom", 6 | "repository": "https://github.com/noseglid/atom-build", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">=1.0.0" 10 | }, 11 | "dependencies": { 12 | "atom-package-deps": "^4.0.1", 13 | "atom-space-pen-views": "^2.0.3", 14 | "cross-spawn": "^4.0.2", 15 | "cson-parser": "^1.3.0", 16 | "getmac": "^1.0.7", 17 | "js-yaml": "^3.4.6", 18 | "term.js": "https://github.com/jeremyramin/term.js/tarball/de1635fc2695e7d8165012d3b1d007d7ce60eea2", 19 | "tree-kill": "^1.0.0", 20 | "xregexp": "^3.1.0" 21 | }, 22 | "devDependencies": { 23 | "atom-build-spec-helpers": "^0.4.0", 24 | "babel-eslint": "^6.0.0", 25 | "eslint": "^2.10.1", 26 | "eslint-config-atom-build": "^3.0.0", 27 | "fs-extra": "^2.1.2", 28 | "temp": "^0.8.1" 29 | }, 30 | "package-deps": [ 31 | "busy-signal" 32 | ], 33 | "consumedServices": { 34 | "builder": { 35 | "versions": { 36 | "^2.0.0": "consumeBuilder" 37 | } 38 | }, 39 | "status-bar": { 40 | "versions": { 41 | "^1.0.0": "consumeStatusBar" 42 | } 43 | }, 44 | "busy-signal": { 45 | "versions": { 46 | "^1.0.0": "consumeBusySignal" 47 | } 48 | }, 49 | "linter-indie": { 50 | "versions": { 51 | "1.0.0": "consumeLinterRegistry" 52 | } 53 | } 54 | }, 55 | "scripts": { 56 | "test": "eslint ." 57 | }, 58 | "keywords": [ 59 | "build", 60 | "compile", 61 | "gulp", 62 | "make", 63 | "productivity" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": [ "error", { "allow": [ "warn", "error" ] } ] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/build-atomCommandName-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import temp from 'temp'; 5 | import specHelpers from 'atom-build-spec-helpers'; 6 | import os from 'os'; 7 | 8 | describe('AtomCommandName', () => { 9 | const originalHomedirFn = os.homedir; 10 | let directory = null; 11 | let workspaceElement = null; 12 | 13 | temp.track(); 14 | 15 | beforeEach(() => { 16 | const createdHomeDir = temp.mkdirSync('atom-build-spec-home'); 17 | os.homedir = () => createdHomeDir; 18 | directory = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })); 19 | atom.project.setPaths([ directory ]); 20 | 21 | atom.config.set('build.buildOnSave', false); 22 | atom.config.set('build.panelVisibility', 'Toggle'); 23 | atom.config.set('build.saveOnBuild', false); 24 | atom.config.set('build.notificationOnRefresh', true); 25 | 26 | jasmine.unspy(window, 'setTimeout'); 27 | jasmine.unspy(window, 'clearTimeout'); 28 | 29 | runs(() => { 30 | workspaceElement = atom.views.getView(atom.workspace); 31 | workspaceElement.setAttribute('style', 'width:9999px'); 32 | jasmine.attachToDOM(workspaceElement); 33 | }); 34 | 35 | waitsForPromise(() => { 36 | return atom.packages.activatePackage('build'); 37 | }); 38 | }); 39 | 40 | afterEach(() => { 41 | os.homedir = originalHomedirFn; 42 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 43 | }); 44 | 45 | describe('when atomCommandName is specified in build config', () => { 46 | it('it should register that command to atom', () => { 47 | fs.writeFileSync(`${directory}/.atom-build.json`, JSON.stringify({ 48 | name: 'The default build', 49 | cmd: 'echo default', 50 | atomCommandName: 'someProvider:customCommand' 51 | })); 52 | 53 | waitsForPromise(() => specHelpers.refreshAwaitTargets()); 54 | 55 | runs(() => atom.commands.dispatch(workspaceElement, 'someProvider:customCommand')); 56 | 57 | waitsFor(() => { 58 | return workspaceElement.querySelector('.build .title') && 59 | workspaceElement.querySelector('.build .title').classList.contains('success'); 60 | }); 61 | 62 | runs(() => { 63 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/default/); 64 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /spec/build-confirm-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import temp from 'temp'; 5 | import os from 'os'; 6 | 7 | describe('Confirm', () => { 8 | let directory = null; 9 | let workspaceElement = null; 10 | const cat = process.platform === 'win32' ? 'type' : 'cat'; 11 | const waitTime = process.env.CI ? 2400 : 200; 12 | const originalHomedirFn = os.homedir; 13 | 14 | temp.track(); 15 | 16 | beforeEach(() => { 17 | const createdHomeDir = temp.mkdirSync('atom-build-spec-home'); 18 | os.homedir = () => createdHomeDir; 19 | directory = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })) + '/'; 20 | atom.project.setPaths([ directory ]); 21 | 22 | atom.config.set('build.buildOnSave', false); 23 | atom.config.set('build.panelVisibility', 'Toggle'); 24 | atom.config.set('build.saveOnBuild', false); 25 | atom.config.set('build.notificationOnRefresh', true); 26 | 27 | jasmine.unspy(window, 'setTimeout'); 28 | jasmine.unspy(window, 'clearTimeout'); 29 | 30 | runs(() => { 31 | workspaceElement = atom.views.getView(atom.workspace); 32 | workspaceElement.setAttribute('style', 'width:9999px'); 33 | jasmine.attachToDOM(workspaceElement); 34 | }); 35 | 36 | waitsForPromise(() => { 37 | return atom.packages.activatePackage('build'); 38 | }); 39 | }); 40 | 41 | afterEach(() => { 42 | os.homedir = originalHomedirFn; 43 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 44 | }); 45 | 46 | describe('when the text editor is modified', () => { 47 | it('should show the save confirmation', () => { 48 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 49 | 50 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 51 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 52 | })); 53 | 54 | waitsForPromise(() => { 55 | return atom.workspace.open('.atom-build.json'); 56 | }); 57 | 58 | runs(() => { 59 | const editor = atom.workspace.getActiveTextEditor(); 60 | editor.insertText('hello kansas'); 61 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 62 | }); 63 | 64 | waitsFor(() => { 65 | return workspaceElement.querySelector('.build-confirm'); 66 | }); 67 | 68 | runs(() => { 69 | expect(document.activeElement.classList.contains('btn-success')).toEqual(true); 70 | }); 71 | }); 72 | 73 | it('should cancel the confirm window when pressing escape', () => { 74 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 75 | 76 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 77 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 78 | })); 79 | 80 | waitsForPromise(() => { 81 | return atom.workspace.open('.atom-build.json'); 82 | }); 83 | 84 | runs(() => { 85 | const editor = atom.workspace.getActiveTextEditor(); 86 | editor.insertText('hello kansas'); 87 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 88 | }); 89 | 90 | waitsFor(() => workspaceElement.querySelector('.build-confirm')); 91 | 92 | runs(() => { 93 | atom.commands.dispatch(workspaceElement, 'build:no-confirm'); 94 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 95 | }); 96 | }); 97 | 98 | it('should not do anything if issuing no-confirm whithout the dialog', () => { 99 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 100 | atom.commands.dispatch(workspaceElement, 'build:no-confirm'); 101 | }); 102 | 103 | it('should not confirm if a TextEditor edits an unsaved file', () => { 104 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 105 | 106 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 107 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 108 | })); 109 | 110 | waitsForPromise(() => { 111 | return Promise.all([ 112 | atom.workspace.open('.atom-build.json'), 113 | atom.workspace.open() 114 | ]); 115 | }); 116 | 117 | runs(() => { 118 | const editor = atom.workspace.getTextEditors().find(textEditor => { 119 | return ('untitled' === textEditor.getTitle()); 120 | }); 121 | editor.insertText('Just some temporary place to write stuff'); 122 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 123 | }); 124 | 125 | waitsFor(() => { 126 | return workspaceElement.querySelector('.build .title') && 127 | workspaceElement.querySelector('.build .title').classList.contains('success'); 128 | }); 129 | 130 | runs(() => { 131 | expect(workspaceElement.querySelector('.build')).toExist(); 132 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/Surprising is the passing of time but not so, as the time of passing/); 133 | }); 134 | }); 135 | 136 | it('should save and build when selecting save and build', () => { 137 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 138 | 139 | fs.writeFileSync(directory + 'catme', 'Surprising is the passing of time but not so, as the time of passing.'); 140 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 141 | cmd: `${cat} catme` 142 | })); 143 | 144 | waitsForPromise(() => atom.workspace.open('catme')); 145 | 146 | runs(() => { 147 | const editor = atom.workspace.getActiveTextEditor(); 148 | editor.setText('kansas'); 149 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 150 | }); 151 | 152 | waitsFor(() => workspaceElement.querySelector('.build-confirm')); 153 | runs(() => document.activeElement.click()); 154 | 155 | waitsFor(() => { 156 | return workspaceElement.querySelector('.build .title') && 157 | workspaceElement.querySelector('.build .title').classList.contains('success'); 158 | }); 159 | 160 | runs(() => { 161 | const editor = atom.workspace.getActiveTextEditor(); 162 | expect(workspaceElement.querySelector('.build')).toExist(); 163 | expect(workspaceElement.querySelector('.build .output').innerHTML).toMatch(/kansas/); 164 | expect(!editor.isModified()); 165 | }); 166 | }); 167 | 168 | it('should build but not save when opting so', () => { 169 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 170 | 171 | fs.writeFileSync(directory + 'catme', 'Surprising is the passing of time but not so, as the time of passing.'); 172 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 173 | cmd: `${cat} catme` 174 | })); 175 | 176 | waitsForPromise(() => atom.workspace.open('catme')); 177 | 178 | runs(() => { 179 | const editor = atom.workspace.getActiveTextEditor(); 180 | editor.setText('catme'); 181 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 182 | }); 183 | 184 | waitsFor(() => workspaceElement.querySelector('.build-confirm')); 185 | 186 | runs(() => { 187 | workspaceElement.querySelector('button[click="confirmWithoutSave"]').click(); 188 | }); 189 | 190 | waitsFor(() => { 191 | return workspaceElement.querySelector('.build .title') && 192 | workspaceElement.querySelector('.build .title').classList.contains('success'); 193 | }); 194 | 195 | runs(() => { 196 | const editor = atom.workspace.getActiveTextEditor(); 197 | expect(workspaceElement.querySelector('.build')).toExist(); 198 | expect(workspaceElement.querySelector('.build .output').innerHTML).not.toMatch(/kansas/); 199 | expect(editor.isModified()); 200 | }); 201 | }); 202 | 203 | it('should do nothing when cancelling', () => { 204 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 205 | 206 | fs.writeFileSync(directory + 'catme', 'Surprising is the passing of time but not so, as the time of passing.'); 207 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 208 | cmd: `${cat} catme` 209 | })); 210 | 211 | waitsForPromise(() => atom.workspace.open('catme')); 212 | 213 | runs(() => { 214 | const editor = atom.workspace.getActiveTextEditor(); 215 | editor.setText('kansas'); 216 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 217 | }); 218 | 219 | waitsFor(() => workspaceElement.querySelector('.build-confirm')); 220 | 221 | runs(() => { 222 | workspaceElement.querySelector('button[click="cancel"]').click(); 223 | }); 224 | 225 | waits(waitTime); 226 | 227 | runs(() => { 228 | const editor = atom.workspace.getActiveTextEditor(); 229 | expect(workspaceElement.querySelector('.build')).not.toExist(); 230 | expect(editor.isModified()); 231 | }); 232 | }); 233 | }); 234 | 235 | describe('when build is triggered without answering confirm dialog', function () { 236 | it('should only keep at maximum 1 dialog open', function () { 237 | expect(workspaceElement.querySelector('.build-confirm')).not.toExist(); 238 | 239 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 240 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 241 | })); 242 | 243 | waitsForPromise(() => atom.workspace.open('.atom-build.json')); 244 | 245 | runs(() => { 246 | const editor = atom.workspace.getActiveTextEditor(); 247 | editor.setText(JSON.stringify({ 248 | cmd: 'echo kansas' 249 | })); 250 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 251 | }); 252 | 253 | waitsFor(() => { 254 | return workspaceElement.querySelector('.build-confirm'); 255 | }); 256 | 257 | runs(() => { 258 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 259 | }); 260 | 261 | waits(waitTime); // Everything is the same so we can't know when second build:trigger has been handled 262 | 263 | runs(() => { 264 | expect(workspaceElement.querySelectorAll('.build-confirm').length).toEqual(1); 265 | }); 266 | }); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /spec/build-hooks-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import temp from 'temp'; 5 | import specHelpers from 'atom-build-spec-helpers'; 6 | 7 | describe('Hooks', () => { 8 | let directory = null; 9 | let workspaceElement = null; 10 | const succeedingCommandName = 'build:hook-test:succeeding'; 11 | const failingCommandName = 'build:hook-test:failing'; 12 | const dummyPackageName = 'atom-build-hooks-dummy-package'; 13 | const dummyPackagePath = __dirname + '/fixture/' + dummyPackageName; 14 | 15 | temp.track(); 16 | 17 | beforeEach(() => { 18 | directory = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })); 19 | atom.project.setPaths([ directory ]); 20 | 21 | atom.config.set('build.buildOnSave', false); 22 | atom.config.set('build.panelVisibility', 'Toggle'); 23 | atom.config.set('build.saveOnBuild', false); 24 | atom.config.set('build.notificationOnRefresh', true); 25 | 26 | jasmine.unspy(window, 'setTimeout'); 27 | jasmine.unspy(window, 'clearTimeout'); 28 | 29 | runs(() => { 30 | workspaceElement = atom.views.getView(atom.workspace); 31 | jasmine.attachToDOM(workspaceElement); 32 | }); 33 | 34 | waitsForPromise(() => { 35 | return Promise.resolve() 36 | .then(() => atom.packages.activatePackage('build')) 37 | .then(() => atom.packages.activatePackage(dummyPackagePath)); 38 | }); 39 | 40 | waitsForPromise(() => specHelpers.refreshAwaitTargets()); 41 | }); 42 | 43 | afterEach(() => { 44 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 45 | }); 46 | 47 | it('should call preBuild', () => { 48 | let pkg; 49 | 50 | runs(() => { 51 | pkg = atom.packages.getActivePackage(dummyPackageName).mainModule; 52 | spyOn(pkg.hooks, 'preBuild'); 53 | 54 | atom.commands.dispatch(workspaceElement, succeedingCommandName); 55 | }); 56 | 57 | waitsFor(() => { 58 | return workspaceElement.querySelector('.build .title'); 59 | }); 60 | 61 | runs(() => { 62 | expect(pkg.hooks.preBuild).toHaveBeenCalled(); 63 | }); 64 | }); 65 | 66 | describe('postBuild', () => { 67 | it('should be called with `true` as an argument when build succeded', () => { 68 | let pkg; 69 | 70 | runs(() => { 71 | pkg = atom.packages.getActivePackage(dummyPackageName).mainModule; 72 | spyOn(pkg.hooks, 'postBuild'); 73 | 74 | atom.commands.dispatch(workspaceElement, succeedingCommandName); 75 | }); 76 | 77 | waitsFor(() => { 78 | return workspaceElement.querySelector('.build .title') && 79 | workspaceElement.querySelector('.build .title').classList.contains('success'); 80 | }); 81 | 82 | runs(() => { 83 | expect(pkg.hooks.postBuild).toHaveBeenCalledWith(true); 84 | }); 85 | }); 86 | 87 | it('should be called with `false` as an argument when build failed', () => { 88 | let pkg; 89 | 90 | runs(() => { 91 | pkg = atom.packages.getActivePackage(dummyPackageName).mainModule; 92 | spyOn(pkg.hooks, 'postBuild'); 93 | 94 | atom.commands.dispatch(workspaceElement, failingCommandName); 95 | }); 96 | 97 | waitsFor(() => { 98 | return workspaceElement.querySelector('.build .title') && 99 | workspaceElement.querySelector('.build .title').classList.contains('error'); 100 | }); 101 | 102 | runs(() => { 103 | expect(pkg.hooks.postBuild).toHaveBeenCalledWith(false); 104 | }); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /spec/build-keymap-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import path from 'path'; 5 | import temp from 'temp'; 6 | import specHelpers from 'atom-build-spec-helpers'; 7 | import os from 'os'; 8 | 9 | describe('Keymap', () => { 10 | const originalHomedirFn = os.homedir; 11 | let directory = null; 12 | let workspaceElement = null; 13 | 14 | temp.track(); 15 | 16 | beforeEach(() => { 17 | const createdHomeDir = temp.mkdirSync('atom-build-spec-home'); 18 | os.homedir = () => createdHomeDir; 19 | directory = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })) + path.sep; 20 | atom.project.setPaths([ directory ]); 21 | 22 | atom.config.set('build.buildOnSave', false); 23 | atom.config.set('build.panelVisibility', 'Toggle'); 24 | atom.config.set('build.saveOnBuild', false); 25 | atom.config.set('build.notificationOnRefresh', true); 26 | 27 | jasmine.unspy(window, 'setTimeout'); 28 | jasmine.unspy(window, 'clearTimeout'); 29 | 30 | runs(() => { 31 | workspaceElement = atom.views.getView(atom.workspace); 32 | workspaceElement.setAttribute('style', 'width:9999px'); 33 | jasmine.attachToDOM(workspaceElement); 34 | }); 35 | 36 | waitsForPromise(() => { 37 | return atom.packages.activatePackage('build'); 38 | }); 39 | }); 40 | 41 | afterEach(() => { 42 | os.homedir = originalHomedirFn; 43 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 44 | }); 45 | 46 | describe('when custom keymap is defined in .atom-build.json', () => { 47 | it('should trigger the build when that key combination is pressed', () => { 48 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 49 | name: 'The default build', 50 | cmd: 'echo default', 51 | targets: { 52 | 'keymapped build': { 53 | cmd: 'echo keymapped', 54 | keymap: 'ctrl-alt-k' 55 | } 56 | } 57 | })); 58 | 59 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 60 | 61 | waitsFor(() => { 62 | return workspaceElement.querySelector('.build .title') && 63 | workspaceElement.querySelector('.build .title').classList.contains('success'); 64 | }); 65 | 66 | runs(() => { 67 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/default/); 68 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 69 | }); 70 | 71 | waitsFor(() => { 72 | return !workspaceElement.querySelector('.build .title'); 73 | }); 74 | 75 | runs(() => { 76 | const key = atom.keymaps.constructor.buildKeydownEvent('k', { ctrl: true, alt: true, target: workspaceElement }); 77 | atom.keymaps.handleKeyboardEvent(key); 78 | }); 79 | 80 | waitsFor(() => { 81 | return workspaceElement.querySelector('.build .title') && 82 | workspaceElement.querySelector('.build .title').classList.contains('success'); 83 | }); 84 | 85 | runs(() => { 86 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/keymapped/); 87 | }); 88 | }); 89 | 90 | it('should not changed the set active build', () => { 91 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 92 | name: 'The default build', 93 | cmd: 'echo default', 94 | targets: { 95 | 'keymapped build': { 96 | cmd: 'echo keymapped', 97 | keymap: 'ctrl-alt-k' 98 | } 99 | } 100 | })); 101 | 102 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 103 | 104 | waitsFor(() => { 105 | return workspaceElement.querySelector('.build .title') && 106 | workspaceElement.querySelector('.build .title').classList.contains('success'); 107 | }); 108 | 109 | runs(() => { 110 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/default/); 111 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 112 | }); 113 | 114 | waitsFor(() => { 115 | return !workspaceElement.querySelector('.build .title'); 116 | }); 117 | 118 | runs(() => { 119 | const key = atom.keymaps.constructor.buildKeydownEvent('k', { ctrl: true, alt: true, target: workspaceElement }); 120 | atom.keymaps.handleKeyboardEvent(key); 121 | }); 122 | 123 | waitsFor(() => { 124 | return workspaceElement.querySelector('.build .title') && 125 | workspaceElement.querySelector('.build .title').classList.contains('success'); 126 | }); 127 | 128 | runs(() => { 129 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/keymapped/); 130 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 131 | }); 132 | 133 | waitsFor(() => { 134 | return !workspaceElement.querySelector('.build .title'); 135 | }); 136 | 137 | runs(() => { 138 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 139 | }); 140 | 141 | waitsFor(() => { 142 | return workspaceElement.querySelector('.build .title') && 143 | workspaceElement.querySelector('.build .title').classList.contains('success'); 144 | }); 145 | 146 | runs(() => { 147 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/default/); 148 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 149 | }); 150 | }); 151 | 152 | it('should dispose keymap when reloading targets', () => { 153 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 154 | name: 'The default build', 155 | cmd: 'echo default', 156 | targets: { 157 | 'keymapped build': { 158 | cmd: 'echo keymapped', 159 | keymap: 'ctrl-alt-k' 160 | } 161 | } 162 | })); 163 | 164 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 165 | 166 | waitsFor(() => { 167 | return workspaceElement.querySelector('.build .title') && 168 | workspaceElement.querySelector('.build .title').classList.contains('success'); 169 | }); 170 | 171 | runs(() => { 172 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/default/); 173 | }); 174 | 175 | waitsFor(() => { 176 | return !workspaceElement.querySelector('.build .title'); 177 | }); 178 | 179 | runs(() => { 180 | const key = atom.keymaps.constructor.buildKeydownEvent('k', { ctrl: true, alt: true, target: workspaceElement }); 181 | atom.keymaps.handleKeyboardEvent(key); 182 | }); 183 | 184 | waitsFor(() => { 185 | return workspaceElement.querySelector('.build .title') && 186 | workspaceElement.querySelector('.build .title').classList.contains('success'); 187 | }); 188 | 189 | runs(() => { 190 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/keymapped/); 191 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 192 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 193 | name: 'The default build', 194 | cmd: 'echo default', 195 | targets: { 196 | 'keymapped build': { 197 | cmd: 'echo ctrl-x new file', 198 | keymap: 'ctrl-x' 199 | } 200 | } 201 | })); 202 | }); 203 | 204 | waitsForPromise(() => specHelpers.awaitTargets()); 205 | 206 | waitsFor(() => { 207 | return !workspaceElement.querySelector('.build .title'); 208 | }); 209 | 210 | runs(() => { 211 | const key = atom.keymaps.constructor.buildKeydownEvent('k', { ctrl: true, alt: true, target: workspaceElement }); 212 | atom.keymaps.handleKeyboardEvent(key); 213 | }); 214 | 215 | waits(300); 216 | 217 | runs(() => { 218 | expect(workspaceElement.querySelector('.build')).not.toExist(); 219 | const key = atom.keymaps.constructor.buildKeydownEvent('x', { ctrl: true, target: workspaceElement }); 220 | atom.keymaps.handleKeyboardEvent(key); 221 | }); 222 | 223 | waitsFor(() => { 224 | return workspaceElement.querySelector('.build .title') && 225 | workspaceElement.querySelector('.build .title').classList.contains('success'); 226 | }); 227 | 228 | runs(() => { 229 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/ctrl-x new file/); 230 | }); 231 | }); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /spec/build-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import path from 'path'; 5 | import temp from 'temp'; 6 | import specHelpers from 'atom-build-spec-helpers'; 7 | import os from 'os'; 8 | import { sleep, cat, shellCmd, waitTime } from './helpers'; 9 | 10 | describe('Build', () => { 11 | const goodAtomBuildfile = __dirname + '/fixture/.atom-build.json'; 12 | const shellAtomBuildfile = __dirname + '/fixture/.atom-build.shell.json'; 13 | const replaceAtomBuildFile = __dirname + '/fixture/.atom-build.replace.json'; 14 | const shFalseAtomBuildFile = __dirname + '/fixture/.atom-build.sh-false.json'; 15 | const shTrueAtomBuildFile = __dirname + '/fixture/.atom-build.sh-true.json'; 16 | const shDefaultAtomBuildFile = __dirname + '/fixture/.atom-build.sh-default.json'; 17 | const syntaxErrorAtomBuildFile = __dirname + '/fixture/.atom-build.syntax-error.json'; 18 | const originalHomedirFn = os.homedir; 19 | 20 | let directory = null; 21 | let workspaceElement = null; 22 | 23 | temp.track(); 24 | 25 | beforeEach(() => { 26 | atom.config.set('build.buildOnSave', false); 27 | atom.config.set('build.panelVisibility', 'Toggle'); 28 | atom.config.set('build.saveOnBuild', false); 29 | atom.config.set('build.stealFocus', true); 30 | atom.config.set('build.notificationOnRefresh', true); 31 | atom.notifications.clear(); 32 | 33 | workspaceElement = atom.views.getView(atom.workspace); 34 | workspaceElement.setAttribute('style', 'width:9999px'); 35 | jasmine.attachToDOM(workspaceElement); 36 | jasmine.unspy(window, 'setTimeout'); 37 | jasmine.unspy(window, 'clearTimeout'); 38 | 39 | waitsForPromise(() => { 40 | return specHelpers.vouch(temp.mkdir, 'atom-build-spec-').then( (dir) => { 41 | return specHelpers.vouch(fs.realpath, dir); 42 | }).then( (dir) => { 43 | directory = dir + path.sep; 44 | atom.project.setPaths([ directory ]); 45 | return specHelpers.vouch(temp.mkdir, 'atom-build-spec-home'); 46 | }).then( (dir) => { 47 | return specHelpers.vouch(fs.realpath, dir); 48 | }).then( (dir) => { 49 | os.homedir = () => dir; 50 | return atom.packages.activatePackage('build'); 51 | }); 52 | }); 53 | }); 54 | 55 | afterEach(() => { 56 | os.homedir = originalHomedirFn; 57 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 58 | }); 59 | 60 | describe('when package is activated', () => { 61 | it('should not show build window if panelVisibility is Toggle ', () => { 62 | expect(workspaceElement.querySelector('.build')).not.toExist(); 63 | }); 64 | }); 65 | 66 | describe('when building', () => { 67 | it('should show build failed if build fails', () => { 68 | expect(workspaceElement.querySelector('.build')).not.toExist(); 69 | 70 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 71 | cmd: 'echo Very bad... && exit 1' 72 | })); 73 | 74 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 75 | 76 | waitsFor(() => { 77 | return workspaceElement.querySelector('.build .title') && 78 | workspaceElement.querySelector('.build .title').classList.contains('error'); 79 | }); 80 | 81 | runs(() => { 82 | expect(workspaceElement.querySelector('.build')).toExist(); 83 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/Very bad\.\.\./); 84 | }); 85 | }); 86 | 87 | it('should fail build, if errors are matched', () => { 88 | expect(workspaceElement.querySelector('.build')).not.toExist(); 89 | 90 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 91 | cmd: 'echo __ERROR__ && exit 0', 92 | errorMatch: 'ERROR' 93 | })); 94 | 95 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 96 | 97 | waitsFor(() => { 98 | return workspaceElement.querySelector('.build .title') && 99 | workspaceElement.querySelector('.build .title').classList.contains('error'); 100 | }); 101 | }); 102 | 103 | it('should cancel build when stopping it, and remove when stopping again', () => { 104 | expect(workspaceElement.querySelector('.build')).not.toExist(); 105 | 106 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 107 | cmd: `echo "Building, this will take some time..." && ${sleep(30)} && echo "Done!"` 108 | })); 109 | 110 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 111 | 112 | // Let build run for one second before we terminate it 113 | waits(1000); 114 | 115 | runs(() => { 116 | expect(workspaceElement.querySelector('.build')).toExist(); 117 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/Building, this will take some time.../); 118 | atom.commands.dispatch(workspaceElement, 'build:stop'); 119 | atom.commands.dispatch(workspaceElement, 'build:stop'); 120 | }); 121 | 122 | waitsFor(() => { 123 | return workspaceElement.querySelector('.build .title') && 124 | workspaceElement.querySelector('.build .title').classList.contains('error'); 125 | }); 126 | 127 | runs(() => { 128 | atom.commands.dispatch(workspaceElement, 'build:stop'); 129 | }); 130 | 131 | waitsFor(() => { 132 | return (!workspaceElement.querySelector('.build .title')); 133 | }); 134 | }); 135 | 136 | it('should not show the build panel if no build file exists', () => { 137 | expect(workspaceElement.querySelector('.build')).not.toExist(); 138 | 139 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 140 | 141 | /* Give it some time here. There's nothing to probe for as we expect the exact same state when done. */ 142 | waits(waitTime); 143 | 144 | runs(() => { 145 | expect(workspaceElement.querySelector('.build')).not.toExist(); 146 | }); 147 | }); 148 | 149 | it('should automatically refresh if build is triggered an no targets are found', () => { 150 | expect(workspaceElement.querySelector('.build')).not.toExist(); 151 | 152 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 153 | cmd: 'echo hello world' 154 | })); 155 | 156 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 157 | 158 | waitsFor(() => 159 | workspaceElement.querySelector('.build .title') && 160 | workspaceElement.querySelector('.build .title').classList.contains('success')); 161 | 162 | runs(() => { 163 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/hello world/); 164 | }); 165 | }); 166 | }); 167 | 168 | describe('when build is triggered twice', () => { 169 | it('should not leave multiple panels behind', () => { 170 | expect(workspaceElement.querySelector('.build')).not.toExist(); 171 | 172 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 173 | 174 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 175 | cmd: 'echo hello world' 176 | })); 177 | 178 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 179 | 180 | waitsFor(() => { 181 | return workspaceElement.querySelector('.build .title').classList.contains('success'); 182 | }); 183 | 184 | waits(50); 185 | 186 | runs(() => { 187 | expect(workspaceElement.querySelectorAll('.bottom.tool-panel.panel-bottom').length).toBe(1); 188 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 189 | }); 190 | 191 | waits(50); 192 | 193 | runs(() => { 194 | expect(workspaceElement.querySelectorAll('.bottom.tool-panel.panel-bottom').length).toBe(1); 195 | }); 196 | }); 197 | }); 198 | 199 | describe('when custom .atom-build.json is available', () => { 200 | it('should show the build window', () => { 201 | expect(workspaceElement.querySelector('.build')).not.toExist(); 202 | 203 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 204 | cmd: cat(), 205 | args: [ '.atom-build.json' ] 206 | })); 207 | 208 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 209 | 210 | waitsFor(() => { 211 | return workspaceElement.querySelector('.build .title') && 212 | workspaceElement.querySelector('.build .title').classList.contains('success'); 213 | }); 214 | 215 | runs(() => { 216 | expect(workspaceElement.querySelector('.build')).toExist(); 217 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/"args":\[".atom-build.json"\]/); 218 | }); 219 | }); 220 | 221 | it('should be possible to exec shell commands with wildcard expansion', () => { 222 | expect(workspaceElement.querySelector('.build')).not.toExist(); 223 | 224 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(shellAtomBuildfile)); 225 | 226 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 227 | 228 | waitsFor(() => { 229 | return workspaceElement.querySelector('.build .title') && 230 | workspaceElement.querySelector('.build .title').classList.contains('success'); 231 | }); 232 | 233 | runs(() => { 234 | expect(workspaceElement.querySelector('.build')).toExist(); 235 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/Good news, everyone!/); 236 | }); 237 | }); 238 | 239 | it('should show sh message if sh is true', () => { 240 | expect(workspaceElement.querySelector('.build')).not.toExist(); 241 | 242 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(shTrueAtomBuildFile)); 243 | 244 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 245 | 246 | waitsFor(() => { 247 | return workspaceElement.querySelector('.build .title') && 248 | workspaceElement.querySelector('.build .title').classList.contains('success'); 249 | }); 250 | 251 | runs(() => { 252 | expect(workspaceElement.querySelector('.build')).toExist(); 253 | expect(workspaceElement.querySelector('.build .heading-text').textContent).toMatch(new RegExp(`^${shellCmd}`)); 254 | }); 255 | }); 256 | 257 | it('should not show sh message if sh is false', () => { 258 | if (process.platform === 'win32') return; 259 | 260 | expect(workspaceElement.querySelector('.build')).not.toExist(); 261 | 262 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(shFalseAtomBuildFile)); 263 | 264 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 265 | 266 | waitsFor(() => { 267 | return workspaceElement.querySelector('.build .title') && 268 | workspaceElement.querySelector('.build .title').classList.contains('success'); 269 | }); 270 | 271 | runs(() => { 272 | expect(workspaceElement.querySelector('.build')).toExist(); 273 | expect(workspaceElement.querySelector('.build .heading-text').textContent).toMatch(/^echo/); 274 | }); 275 | }); 276 | 277 | it('should show sh message if sh is unspecified', () => { 278 | expect(workspaceElement.querySelector('.build')).not.toExist(); 279 | 280 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(shDefaultAtomBuildFile)); 281 | 282 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 283 | 284 | waitsFor(() => { 285 | return workspaceElement.querySelector('.build .title') && 286 | workspaceElement.querySelector('.build .title').classList.contains('success'); 287 | }); 288 | 289 | runs(() => { 290 | expect(workspaceElement.querySelector('.build')).toExist(); 291 | expect(workspaceElement.querySelector('.build .heading-text').textContent).toMatch(new RegExp(`^${shellCmd}`)); 292 | }); 293 | }); 294 | 295 | it('should show graphical error message if build-file contains syntax errors', () => { 296 | expect(workspaceElement.querySelector('.build')).not.toExist(); 297 | 298 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(syntaxErrorAtomBuildFile)); 299 | 300 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 301 | 302 | waitsFor(() => { 303 | return atom.notifications.getNotifications().find(n => n.getType() === 'error'); 304 | }); 305 | 306 | runs(() => { 307 | const notification = atom.notifications.getNotifications().find(n => n.getType() === 'error'); 308 | expect(notification.getType()).toEqual('error'); 309 | expect(notification.getMessage()).toEqual('Invalid build file.'); 310 | expect(notification.options.detail).toMatch(/Unexpected token t/); 311 | }); 312 | }); 313 | 314 | it('should not cache the contents of the build file', () => { 315 | expect(workspaceElement.querySelector('.build')).not.toExist(); 316 | 317 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 318 | cmd: 'echo first' 319 | })); 320 | 321 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 322 | 323 | waitsFor(() => { 324 | return workspaceElement.querySelector('.build .title') && 325 | workspaceElement.querySelector('.build .title').classList.contains('success'); 326 | }); 327 | 328 | runs(() => { 329 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/first/); 330 | }); 331 | 332 | waitsFor(() => { 333 | return !workspaceElement.querySelector('.build .title'); 334 | }); 335 | 336 | runs(() => { 337 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 338 | cmd: 'echo second' 339 | })); 340 | }); 341 | 342 | waitsForPromise(() => specHelpers.refreshAwaitTargets()); 343 | 344 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 345 | 346 | waitsFor(() => { 347 | return workspaceElement.querySelector('.build .title') && 348 | workspaceElement.querySelector('.build .title').classList.contains('success'); 349 | }); 350 | 351 | runs(() => { 352 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/second/); 353 | }); 354 | }); 355 | }); 356 | 357 | describe('when replacements are specified in the atom-build.json file', () => { 358 | it('should replace those with their dynamic value', () => { 359 | expect(workspaceElement.querySelector('.build')).not.toExist(); 360 | 361 | process.env.FROM_PROCESS_ENV = '{FILE_ACTIVE}'; 362 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(replaceAtomBuildFile)); 363 | 364 | waitsForPromise(() => atom.workspace.open('.atom-build.json')); 365 | 366 | runs(() => atom.workspace.getActiveTextEditor().setSelectedBufferRange([[1, 3], [1, 6]])); 367 | 368 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 369 | 370 | waitsFor(() => { 371 | return workspaceElement.querySelector('.build .title') && 372 | workspaceElement.querySelector('.build .title').classList.contains('success'); 373 | }); 374 | 375 | runs(() => { 376 | expect(workspaceElement.querySelector('.build')).toExist(); 377 | const output = workspaceElement.querySelector('.terminal').terminal.getContent(); 378 | expect(output.indexOf('PROJECT_PATH=' + directory.substring(0, -1))).not.toBe(-1); 379 | expect(output.indexOf('FILE_ACTIVE=' + directory + '.atom-build.json')).not.toBe(-1); 380 | expect(output.indexOf('FROM_ENV=' + directory + '.atom-build.json')).not.toBe(-1); 381 | expect(output.indexOf('FROM_PROCESS_ENV=' + directory + '.atom-build.json')).not.toBe(-1); 382 | expect(output.indexOf('FILE_ACTIVE_NAME=.atom-build.json')).not.toBe(-1); 383 | expect(output.indexOf('FILE_ACTIVE_NAME_BASE=.atom-build')).not.toBe(-1); 384 | expect(output.indexOf('FILE_ACTIVE_CURSOR_ROW=2')).not.toBe(-1); 385 | expect(output.indexOf('FILE_ACTIVE_CURSOR_COLUMN=7')).not.toBe(-1); 386 | expect(output.indexOf('SELECTION=cmd')).not.toBe(-1); 387 | }); 388 | }); 389 | }); 390 | 391 | describe('when the text editor is saved', () => { 392 | it('should build when buildOnSave is true', () => { 393 | atom.config.set('build.buildOnSave', true); 394 | 395 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 396 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 397 | })); 398 | 399 | waitsForPromise(() => atom.workspace.open('dummy')); 400 | 401 | waitsForPromise(() => { 402 | const editor = atom.workspace.getActiveTextEditor(); 403 | return editor.save(); 404 | }); 405 | 406 | waitsFor(() => { 407 | return workspaceElement.querySelector('.build .title') && 408 | workspaceElement.querySelector('.build .title').classList.contains('success'); 409 | }); 410 | 411 | runs(() => { 412 | expect(workspaceElement.querySelector('.build')).toExist(); 413 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/Surprising is the passing of time but not so, as the time of passing/); 414 | }); 415 | }); 416 | 417 | it('should not build when buildOnSave is false', () => { 418 | atom.config.set('build.buildOnSave', false); 419 | 420 | fs.writeFileSync(directory + '.atom-build.json', { 421 | cmd: 'echo "hello, world"' 422 | }); 423 | 424 | waitsForPromise(() => atom.workspace.open('dummy')); 425 | 426 | waitsForPromise(() => { 427 | const editor = atom.workspace.getActiveTextEditor(); 428 | return editor.save(); 429 | }); 430 | 431 | runs(() => { 432 | expect(workspaceElement.querySelector('.build')).not.toExist(); 433 | }); 434 | }); 435 | 436 | it('should not attempt to build if buildOnSave is true and no build tool exists', () => { 437 | atom.config.set('build.buildOnSave', true); 438 | 439 | fs.writeFileSync(directory + '.atom-build.json', { 440 | cmd: 'echo "hello, world"' 441 | }); 442 | 443 | waitsForPromise(() => { 444 | return atom.workspace.open('dummy'); 445 | }); 446 | 447 | runs(() => { 448 | atom.workspace.getActiveTextEditor().save(); 449 | }); 450 | 451 | waits(waitTime); 452 | 453 | runs(() => { 454 | expect(workspaceElement.querySelector('.build')).not.toExist(); 455 | }); 456 | }); 457 | }); 458 | 459 | describe('when multiple project roots are open', () => { 460 | it('should run the second root if a file there is active', () => { 461 | const directory2 = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })) + '/'; 462 | atom.project.addPath(directory2); 463 | expect(workspaceElement.querySelector('.build')).not.toExist(); 464 | 465 | fs.writeFileSync(directory2 + '.atom-build.json', JSON.stringify({ 466 | cmd: cat(), 467 | args: [ '.atom-build.json' ] 468 | })); 469 | 470 | waitsForPromise(() => atom.workspace.open(directory2 + '/main.c')); 471 | 472 | runs(() => { 473 | atom.workspace.getActiveTextEditor().save(); 474 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 475 | }); 476 | 477 | waitsFor(() => { 478 | return workspaceElement.querySelector('.build .title') && 479 | workspaceElement.querySelector('.build .title').classList.contains('success'); 480 | }); 481 | 482 | runs(() => { 483 | expect(workspaceElement.querySelector('.build')).toExist(); 484 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/"args":\[".atom-build.json"\]/); 485 | }); 486 | }); 487 | 488 | it('should scan new project roots when they are added', () => { 489 | const directory2 = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })) + '/'; 490 | fs.writeFileSync(directory2 + '.atom-build.json', JSON.stringify({ 491 | cmd: cat(), 492 | args: [ '.atom-build.json' ] 493 | })); 494 | 495 | waitsForPromise(() => specHelpers.refreshAwaitTargets()); 496 | 497 | waitsForPromise(() => { 498 | const promise = specHelpers.awaitTargets(); 499 | atom.project.addPath(directory2); 500 | return Promise.all([ promise, atom.workspace.open(directory2 + '/main.c') ]); 501 | }); 502 | 503 | runs(() => { 504 | atom.workspace.getActiveTextEditor().save(); 505 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 506 | }); 507 | 508 | waitsFor(() => { 509 | return workspaceElement.querySelector('.build .title') && 510 | workspaceElement.querySelector('.build .title').classList.contains('success'); 511 | }); 512 | 513 | runs(() => { 514 | expect(workspaceElement.querySelector('.build')).toExist(); 515 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/"args":\[".atom-build.json"\]/); 516 | }); 517 | }); 518 | }); 519 | 520 | describe('when build panel is toggled and it is not visible', () => { 521 | it('should show the build panel', () => { 522 | expect(workspaceElement.querySelector('.build')).not.toExist(); 523 | 524 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 525 | 526 | expect(workspaceElement.querySelector('.build')).toExist(); 527 | }); 528 | }); 529 | 530 | describe('when build is triggered, focus should adhere the stealFocus config', () => { 531 | it('should focus the build panel if stealFocus is true', () => { 532 | expect(workspaceElement.querySelector('.build')).not.toExist(); 533 | 534 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(goodAtomBuildfile)); 535 | 536 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 537 | 538 | waitsFor(() => { 539 | return workspaceElement.querySelector('.build'); 540 | }); 541 | 542 | runs(() => { 543 | expect(document.activeElement).toHaveClass('build'); 544 | }); 545 | }); 546 | 547 | it('should leave focus untouched if stealFocus is false', () => { 548 | expect(workspaceElement.querySelector('.build')).not.toExist(); 549 | 550 | atom.config.set('build.stealFocus', false); 551 | const activeElement = document.activeElement; 552 | 553 | fs.writeFileSync(directory + '.atom-build.json', fs.readFileSync(goodAtomBuildfile)); 554 | 555 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 556 | 557 | waitsFor(() => { 558 | return workspaceElement.querySelector('.build'); 559 | }); 560 | 561 | runs(() => { 562 | expect(document.activeElement).toEqual(activeElement); 563 | expect(document.activeElement).not.toHaveClass('build'); 564 | }); 565 | }); 566 | }); 567 | 568 | describe('when no build tools are available', () => { 569 | it('should show a warning', () => { 570 | expect(workspaceElement.querySelector('.build')).not.toExist(); 571 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 572 | 573 | waitsFor(() => atom.notifications.getNotifications().find(n => n.getMessage() === 'No eligible build target.')); 574 | 575 | runs(() => { 576 | const notification = atom.notifications.getNotifications().find(n => n.getMessage() === 'No eligible build target.'); 577 | expect(notification.getType()).toEqual('warning'); 578 | }); 579 | }); 580 | }); 581 | }); 582 | -------------------------------------------------------------------------------- /spec/build-targets-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import temp from 'temp'; 5 | import path from 'path'; 6 | import specHelpers from 'atom-build-spec-helpers'; 7 | import os from 'os'; 8 | 9 | describe('Target', () => { 10 | const originalHomedirFn = os.homedir; 11 | let directory = null; 12 | let workspaceElement = null; 13 | 14 | temp.track(); 15 | 16 | beforeEach(() => { 17 | atom.config.set('build.buildOnSave', false); 18 | atom.config.set('build.panelVisibility', 'Toggle'); 19 | atom.config.set('build.saveOnBuild', false); 20 | atom.config.set('build.notificationOnRefresh', true); 21 | atom.config.set('build.refreshOnShowTargetList', true); 22 | 23 | jasmine.unspy(window, 'setTimeout'); 24 | jasmine.unspy(window, 'clearTimeout'); 25 | 26 | workspaceElement = atom.views.getView(atom.workspace); 27 | workspaceElement.setAttribute('style', 'width:9999px'); 28 | jasmine.attachToDOM(workspaceElement); 29 | 30 | waitsForPromise(() => { 31 | return specHelpers.vouch(temp.mkdir, { prefix: 'atom-build-spec-' }).then((dir) => { 32 | return specHelpers.vouch(fs.realpath, dir); 33 | }).then((dir) => { 34 | directory = dir + '/'; 35 | atom.project.setPaths([ directory ]); 36 | return specHelpers.vouch(temp.mkdir, 'atom-build-spec-home'); 37 | }).then( (dir) => { 38 | return specHelpers.vouch(fs.realpath, dir); 39 | }).then( (dir) => { 40 | os.homedir = () => dir; 41 | return atom.packages.activatePackage('build'); 42 | }); 43 | }); 44 | }); 45 | 46 | afterEach(() => { 47 | os.homedir = originalHomedirFn; 48 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 49 | }); 50 | 51 | describe('when no targets exists', () => { 52 | it('should show a notification', () => { 53 | runs(() => { 54 | atom.commands.dispatch(workspaceElement, 'build:select-active-target'); 55 | }); 56 | 57 | waitsFor(() => { 58 | return workspaceElement.querySelector('.select-list.build-target'); 59 | }); 60 | 61 | runs(() => { 62 | const targets = [ ...workspaceElement.querySelectorAll('.select-list li.build-target') ].map(el => el.textContent); 63 | expect(targets).toEqual([]); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('when multiple targets exists', () => { 69 | it('should list those targets in a SelectListView (from .atom-build.json)', () => { 70 | waitsForPromise(() => { 71 | const file = __dirname + '/fixture/.atom-build.targets.json'; 72 | return specHelpers.vouch(fs.copy, file, directory + '/.atom-build.json'); 73 | }); 74 | 75 | runs(() => { 76 | atom.commands.dispatch(workspaceElement, 'build:select-active-target'); 77 | }); 78 | 79 | waitsFor(() => { 80 | return workspaceElement.querySelector('.select-list li.build-target'); 81 | }); 82 | 83 | runs(() => { 84 | const targets = [ ...workspaceElement.querySelectorAll('.select-list li.build-target') ].map(el => el.textContent); 85 | expect(targets).toEqual([ 'Custom: The default build', 'Custom: Some customized build' ]); 86 | }); 87 | }); 88 | 89 | it('should mark the first target as active', () => { 90 | waitsForPromise(() => { 91 | const file = __dirname + '/fixture/.atom-build.targets.json'; 92 | return specHelpers.vouch(fs.copy, file, directory + '/.atom-build.json'); 93 | }); 94 | 95 | runs(() => { 96 | atom.commands.dispatch(workspaceElement, 'build:select-active-target'); 97 | }); 98 | 99 | waitsFor(() => { 100 | return workspaceElement.querySelector('.select-list li.build-target'); 101 | }); 102 | 103 | runs(() => { 104 | const el = workspaceElement.querySelector('.select-list li.build-target'); // querySelector selects the first element 105 | expect(el).toHaveClass('selected'); 106 | expect(el).toHaveClass('active'); 107 | }); 108 | }); 109 | 110 | it('should run the selected build', () => { 111 | waitsForPromise(() => { 112 | const file = __dirname + '/fixture/.atom-build.targets.json'; 113 | return specHelpers.vouch(fs.copy, file, directory + '/.atom-build.json'); 114 | }); 115 | 116 | runs(() => { 117 | atom.commands.dispatch(workspaceElement, 'build:select-active-target'); 118 | }); 119 | 120 | waitsFor(() => { 121 | return workspaceElement.querySelector('.select-list li.build-target'); 122 | }); 123 | 124 | runs(() => { 125 | atom.commands.dispatch(workspaceElement.querySelector('.select-list'), 'core:confirm'); 126 | }); 127 | 128 | waitsFor(() => { 129 | return workspaceElement.querySelector('.build .title') && 130 | workspaceElement.querySelector('.build .title').classList.contains('success'); 131 | }); 132 | 133 | runs(() => { 134 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/default/); 135 | }); 136 | }); 137 | 138 | it('should run the default target if no selection has been made', () => { 139 | waitsForPromise(() => { 140 | const file = __dirname + '/fixture/.atom-build.targets.json'; 141 | return specHelpers.vouch(fs.copy, file, directory + '/.atom-build.json'); 142 | }); 143 | 144 | runs(() => { 145 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 146 | }); 147 | 148 | waitsFor(() => { 149 | return workspaceElement.querySelector('.build .title') && 150 | workspaceElement.querySelector('.build .title').classList.contains('success'); 151 | }); 152 | 153 | runs(() => { 154 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/default/); 155 | }); 156 | }); 157 | 158 | it('run the selected target if selection has changed, and subsequent build should run that target', () => { 159 | waitsForPromise(() => { 160 | const file = __dirname + '/fixture/.atom-build.targets.json'; 161 | return specHelpers.vouch(fs.copy, file, directory + '/.atom-build.json'); 162 | }); 163 | 164 | runs(() => { 165 | atom.commands.dispatch(workspaceElement, 'build:select-active-target'); 166 | }); 167 | 168 | waitsFor(() => { 169 | return workspaceElement.querySelector('.select-list li.build-target'); 170 | }); 171 | 172 | runs(() => { 173 | atom.commands.dispatch(workspaceElement.querySelector('.select-list'), 'core:move-down'); 174 | }); 175 | 176 | waitsFor(() => { 177 | return workspaceElement.querySelector('.select-list li.selected').textContent === 'Custom: Some customized build'; 178 | }); 179 | 180 | runs(() => { 181 | atom.commands.dispatch(workspaceElement.querySelector('.select-list'), 'core:confirm'); 182 | }); 183 | 184 | waitsFor(() => { 185 | return workspaceElement.querySelector('.build .title') && 186 | workspaceElement.querySelector('.build .title').classList.contains('success'); 187 | }); 188 | 189 | runs(() => { 190 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/customized/); 191 | atom.commands.dispatch(workspaceElement.querySelector('.build'), 'build:stop'); 192 | }); 193 | 194 | waitsFor(() => { 195 | return !workspaceElement.querySelector('.build'); 196 | }); 197 | 198 | runs(() => { 199 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 200 | }); 201 | 202 | waitsFor(() => { 203 | return workspaceElement.querySelector('.build .title') && 204 | workspaceElement.querySelector('.build .title').classList.contains('success'); 205 | }); 206 | 207 | runs(() => { 208 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/customized/); 209 | }); 210 | }); 211 | 212 | it('should show a warning if current file is not part of an open Atom project', () => { 213 | waitsForPromise(() => atom.workspace.open(path.join('..', 'randomFile'))); 214 | waitsForPromise(() => specHelpers.refreshAwaitTargets()); 215 | runs(() => atom.commands.dispatch(workspaceElement, 'build:select-active-target')); 216 | waitsFor(() => atom.notifications.getNotifications().find(n => n.message === 'Unable to build.')); 217 | runs(() => { 218 | const not = atom.notifications.getNotifications().find(n => n.message === 'Unable to build.'); 219 | expect(not.type).toBe('warning'); 220 | }); 221 | }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /spec/build-view-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import temp from 'temp'; 5 | import specHelpers from 'atom-build-spec-helpers'; 6 | import os from 'os'; 7 | import { sleep } from './helpers'; 8 | 9 | describe('BuildView', () => { 10 | const originalHomedirFn = os.homedir; 11 | let directory = null; 12 | let workspaceElement = null; 13 | 14 | temp.track(); 15 | 16 | beforeEach(() => { 17 | atom.config.set('build.buildOnSave', false); 18 | atom.config.set('build.panelVisibility', 'Toggle'); 19 | atom.config.set('build.saveOnBuild', false); 20 | atom.config.set('build.stealFocus', true); 21 | atom.config.set('build.notificationOnRefresh', true); 22 | atom.config.set('editor.fontSize', 14); 23 | atom.notifications.clear(); 24 | 25 | workspaceElement = atom.views.getView(atom.workspace); 26 | workspaceElement.setAttribute('style', 'width:9999px'); 27 | jasmine.attachToDOM(workspaceElement); 28 | jasmine.unspy(window, 'setTimeout'); 29 | jasmine.unspy(window, 'clearTimeout'); 30 | 31 | runs(() => { 32 | workspaceElement = atom.views.getView(atom.workspace); 33 | jasmine.attachToDOM(workspaceElement); 34 | }); 35 | 36 | waitsForPromise(() => { 37 | return specHelpers.vouch(temp.mkdir, { prefix: 'atom-build-spec-' }).then( (dir) => { 38 | return specHelpers.vouch(fs.realpath, dir); 39 | }).then( (dir) => { 40 | directory = dir + '/'; 41 | atom.project.setPaths([ directory ]); 42 | return specHelpers.vouch(temp.mkdir, 'atom-build-spec-home'); 43 | }).then( (dir) => { 44 | return specHelpers.vouch(fs.realpath, dir); 45 | }).then( (dir) => { 46 | os.homedir = () => dir; 47 | return atom.packages.activatePackage('build'); 48 | }); 49 | }); 50 | }); 51 | 52 | afterEach(() => { 53 | os.homedir = originalHomedirFn; 54 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 55 | }); 56 | 57 | describe('when output from build command should be viewed', () => { 58 | it('should output data even if no line break exists', () => { 59 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 60 | cmd: 'node', 61 | args: [ '-e', 'process.stdout.write(\'data without linebreak\');' ], 62 | sh: false 63 | })); 64 | 65 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 66 | 67 | waitsFor(() => { 68 | return workspaceElement.querySelector('.build .title') && 69 | workspaceElement.querySelector('.build .title').classList.contains('success'); 70 | }); 71 | 72 | runs(() => { 73 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/data without linebreak/); 74 | }); 75 | }); 76 | 77 | it('should escape HTML chars so the output is not garbled or missing', () => { 78 | expect(workspaceElement.querySelector('.build')).not.toExist(); 79 | 80 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 81 | cmd: 'echo ""' 82 | })); 83 | 84 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 85 | 86 | waitsFor(() => { 87 | return workspaceElement.querySelector('.build .title') && 88 | workspaceElement.querySelector('.build .title').classList.contains('success'); 89 | }); 90 | 91 | runs(() => { 92 | expect(workspaceElement.querySelector('.build')).toExist(); 93 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(//); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('when a build is triggered', () => { 99 | it('should include a timer of the build', () => { 100 | expect(workspaceElement.querySelector('.build')).not.toExist(); 101 | 102 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 103 | cmd: `echo "Building, this will take some time..." && ${sleep(30)} && echo "Done!"` 104 | })); 105 | 106 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 107 | 108 | // Let build run for 1.5 second. This should set the timer at "at least" 1.5 109 | // which is expected below. If this waits longer than 2000 ms, we're in trouble. 110 | waits(1500); 111 | 112 | runs(() => { 113 | expect(workspaceElement.querySelector('.build-timer').textContent).toMatch(/1.\d/); 114 | 115 | // stop twice to abort the build 116 | atom.commands.dispatch(workspaceElement, 'build:stop'); 117 | atom.commands.dispatch(workspaceElement, 'build:stop'); 118 | }); 119 | }); 120 | }); 121 | 122 | describe('when panel orientation is altered', () => { 123 | it('should show the panel at the bottom spot', () => { 124 | expect(workspaceElement.querySelector('.build')).not.toExist(); 125 | atom.config.set('build.panelOrientation', 'Bottom'); 126 | 127 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 128 | cmd: 'echo this will fail && exit 1' 129 | })); 130 | 131 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 132 | 133 | waitsFor(() => { 134 | return workspaceElement.querySelector('.build .title') && 135 | workspaceElement.querySelector('.build .title').classList.contains('error'); 136 | }); 137 | 138 | runs(() => { 139 | const bottomPanels = atom.workspace.getBottomPanels(); 140 | expect(bottomPanels.length).toEqual(1); 141 | expect(bottomPanels[0].item.constructor.name).toEqual('BuildView'); 142 | }); 143 | }); 144 | 145 | it('should show the panel at the top spot', () => { 146 | expect(workspaceElement.querySelector('.build')).not.toExist(); 147 | atom.config.set('build.panelOrientation', 'Top'); 148 | 149 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 150 | cmd: 'echo this will fail && exit 1' 151 | })); 152 | 153 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 154 | 155 | waitsFor(() => { 156 | return workspaceElement.querySelector('.build .title') && 157 | workspaceElement.querySelector('.build .title').classList.contains('error'); 158 | }); 159 | 160 | runs(() => { 161 | const panels = atom.workspace.getTopPanels(); 162 | expect(panels.length).toEqual(1); 163 | expect(panels[0].item.constructor.name).toEqual('BuildView'); 164 | }); 165 | }); 166 | }); 167 | 168 | describe('when build fails', () => { 169 | it('should keep the build scrolled to bottom', () => { 170 | expect(workspaceElement.querySelector('.build')).not.toExist(); 171 | 172 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 173 | cmd: 'echo a && echo b && echo c && echo d && echo e && echo f && echo g && echo h && exit 1' 174 | })); 175 | 176 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 177 | 178 | waitsFor(() => { 179 | return workspaceElement.querySelector('.build .title') && 180 | workspaceElement.querySelector('.build .title').classList.contains('error'); 181 | }); 182 | 183 | runs(() => { 184 | expect(workspaceElement.querySelector('.terminal').terminal.ydisp).toBeGreaterThan(0); 185 | }); 186 | }); 187 | }); 188 | 189 | describe('when hidePanelHeading is set', () => { 190 | beforeEach(() => { 191 | atom.config.set('build.hidePanelHeading', true); 192 | }); 193 | 194 | afterEach(() => { 195 | atom.config.set('build.hidePanelHeading', false); 196 | }); 197 | 198 | it('should not show the panel heading', () => { 199 | expect(workspaceElement.querySelector('.build')).not.toExist(); 200 | 201 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 202 | cmd: 'echo hello && exit 1' 203 | })); 204 | 205 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 206 | 207 | waitsFor(() => { 208 | return workspaceElement.querySelector('.build'); 209 | }); 210 | 211 | runs(() => { 212 | expect(workspaceElement.querySelector('.build .heading')).toBeHidden(); 213 | }); 214 | }); 215 | 216 | it('should show the heading when hidden is disabled', () => { 217 | expect(workspaceElement.querySelector('.build')).not.toExist(); 218 | 219 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 220 | cmd: 'echo hello && exit 1' 221 | })); 222 | 223 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 224 | 225 | waitsFor(() => { 226 | return workspaceElement.querySelector('.build'); 227 | }); 228 | 229 | runs(() => { 230 | expect(workspaceElement.querySelector('.build .heading')).toBeHidden(); 231 | atom.config.set('build.hidePanelHeading', false); 232 | expect(workspaceElement.querySelector('.build .heading')).toBeVisible(); 233 | }); 234 | }); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /spec/build-visible-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import temp from 'temp'; 5 | import specHelpers from 'atom-build-spec-helpers'; 6 | import os from 'os'; 7 | 8 | describe('Visible', () => { 9 | let directory = null; 10 | let workspaceElement = null; 11 | const waitTime = process.env.CI ? 2400 : 200; 12 | const originalHomedirFn = os.homedir; 13 | 14 | temp.track(); 15 | 16 | beforeEach(() => { 17 | atom.config.set('build.buildOnSave', false); 18 | atom.config.set('build.panelVisibility', 'Toggle'); 19 | atom.config.set('build.saveOnBuild', false); 20 | atom.config.set('build.stealFocus', true); 21 | atom.config.set('build.notificationOnRefresh', true); 22 | atom.notifications.clear(); 23 | 24 | workspaceElement = atom.views.getView(atom.workspace); 25 | workspaceElement.setAttribute('style', 'width:9999px'); 26 | jasmine.attachToDOM(workspaceElement); 27 | jasmine.unspy(window, 'setTimeout'); 28 | jasmine.unspy(window, 'clearTimeout'); 29 | 30 | runs(() => { 31 | workspaceElement = atom.views.getView(atom.workspace); 32 | jasmine.attachToDOM(workspaceElement); 33 | }); 34 | 35 | waitsForPromise(() => { 36 | return specHelpers.vouch(temp.mkdir, { prefix: 'atom-build-spec-' }).then( (dir) => { 37 | return specHelpers.vouch(fs.realpath, dir); 38 | }).then( (dir) => { 39 | directory = dir + '/'; 40 | atom.project.setPaths([ directory ]); 41 | return specHelpers.vouch(temp.mkdir, 'atom-build-spec-home'); 42 | }).then( (dir) => { 43 | return specHelpers.vouch(fs.realpath, dir); 44 | }).then( (dir) => { 45 | os.homedir = () => dir; 46 | }); 47 | }); 48 | }); 49 | 50 | afterEach(() => { 51 | os.homedir = originalHomedirFn; 52 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 53 | }); 54 | 55 | describe('when package is activated with panel visibility set to Keep Visible', () => { 56 | beforeEach(() => { 57 | atom.config.set('build.panelVisibility', 'Keep Visible'); 58 | waitsForPromise(() => { 59 | return atom.packages.activatePackage('build'); 60 | }); 61 | }); 62 | 63 | it('should show build window', () => { 64 | expect(workspaceElement.querySelector('.build')).toExist(); 65 | }); 66 | }); 67 | 68 | describe('when package is activated with panel visibility set to Toggle', () => { 69 | beforeEach(() => { 70 | atom.config.set('build.panelVisibility', 'Toggle'); 71 | waitsForPromise(() => { 72 | return atom.packages.activatePackage('build'); 73 | }); 74 | }); 75 | 76 | describe('when build panel is toggled and it is visible', () => { 77 | beforeEach(() => { 78 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 79 | }); 80 | 81 | it('should hide the build panel', () => { 82 | expect(workspaceElement.querySelector('.build')).toExist(); 83 | 84 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 85 | 86 | expect(workspaceElement.querySelector('.build')).not.toExist(); 87 | }); 88 | }); 89 | 90 | describe('when panel visibility is set to Show on Error', () => { 91 | it('should only show the build panel if a build fails', () => { 92 | atom.config.set('build.panelVisibility', 'Show on Error'); 93 | 94 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 95 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 96 | })); 97 | 98 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 99 | 100 | /* Give it some reasonable time to show itself if there is a bug */ 101 | waits(waitTime); 102 | 103 | runs(() => { 104 | expect(workspaceElement.querySelector('.build')).not.toExist(); 105 | }); 106 | 107 | runs(() => { 108 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 109 | cmd: 'echo Very bad... && exit 1' 110 | })); 111 | }); 112 | 113 | // .atom-build.json is updated asynchronously... give it some time 114 | waitsForPromise(() => specHelpers.refreshAwaitTargets()); 115 | 116 | runs(() => { 117 | atom.commands.dispatch(workspaceElement, 'build:trigger'); 118 | }); 119 | 120 | waitsFor(() => { 121 | return workspaceElement.querySelector('.build .title') && 122 | workspaceElement.querySelector('.build .title').classList.contains('error'); 123 | }); 124 | 125 | waits(waitTime); 126 | 127 | runs(() => { 128 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/Very bad\.\.\./); 129 | }); 130 | }); 131 | }); 132 | 133 | describe('when panel visibility is set to Hidden', () => { 134 | it('should not show the build panel if build succeeeds', () => { 135 | atom.config.set('build.panelVisibility', 'Hidden'); 136 | 137 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 138 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 139 | })); 140 | 141 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 142 | 143 | /* Give it some reasonable time to show itself if there is a bug */ 144 | waits(waitTime); 145 | 146 | runs(() => { 147 | expect(workspaceElement.querySelector('.build')).not.toExist(); 148 | }); 149 | }); 150 | 151 | it('should not show the build panel if build fails', () => { 152 | atom.config.set('build.panelVisibility', 'Hidden'); 153 | 154 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 155 | cmd: 'echo "Very bad..." && exit 2' 156 | })); 157 | 158 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 159 | 160 | /* Give it some reasonable time to show itself if there is a bug */ 161 | waits(waitTime); 162 | 163 | runs(() => { 164 | expect(workspaceElement.querySelector('.build')).not.toExist(); 165 | }); 166 | }); 167 | 168 | it('should show the build panel if it is toggled', () => { 169 | atom.config.set('build.panelVisibility', 'Hidden'); 170 | 171 | fs.writeFileSync(directory + '.atom-build.json', JSON.stringify({ 172 | cmd: 'echo Surprising is the passing of time but not so, as the time of passing.' 173 | })); 174 | 175 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 176 | 177 | waits(waitTime); // Let build finish. Since UI component is not visible yet, there's nothing to poll. 178 | 179 | runs(() => { 180 | atom.commands.dispatch(workspaceElement, 'build:toggle-panel'); 181 | }); 182 | 183 | waitsFor(() => { 184 | return workspaceElement.querySelector('.build .title') && 185 | workspaceElement.querySelector('.build .title').classList.contains('success'); 186 | }); 187 | 188 | runs(() => { 189 | expect(workspaceElement.querySelector('.terminal').terminal.getContent()).toMatch(/Surprising is the passing of time but not so, as the time of passing/); 190 | }); 191 | }); 192 | }); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /spec/custom-provider-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import fs from 'fs-extra'; 4 | import temp from 'temp'; 5 | import CustomFile from '../lib/atom-build.js'; 6 | import os from 'os'; 7 | 8 | describe('custom provider', () => { 9 | const originalHomedirFn = os.homedir; 10 | let builder; 11 | let directory = null; 12 | let createdHomeDir; 13 | 14 | temp.track(); 15 | 16 | beforeEach(() => { 17 | createdHomeDir = temp.mkdirSync('atom-build-spec-home'); 18 | os.homedir = () => createdHomeDir; 19 | directory = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })) + '/'; 20 | atom.project.setPaths([ directory ]); 21 | builder = new CustomFile(directory); 22 | }); 23 | 24 | afterEach(() => { 25 | os.homedir = originalHomedirFn; 26 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 27 | }); 28 | 29 | describe('when there is no .atom-build config file in any elegible directory', () => { 30 | it('should not be eligible', () => { 31 | expect(builder.isEligible()).toEqual(false); 32 | }); 33 | }); 34 | 35 | describe('when .atom-build config is on home directory', () => { 36 | it('should find json file in home directory', () => { 37 | fs.writeFileSync(createdHomeDir + '/.atom-build.json', fs.readFileSync(__dirname + '/fixture/.atom-build.json')); 38 | expect(builder.isEligible()).toEqual(true); 39 | }); 40 | it('should find cson file in home directory', () => { 41 | fs.writeFileSync(createdHomeDir + '/.atom-build.cson', fs.readFileSync(__dirname + '/fixture/.atom-build.cson')); 42 | expect(builder.isEligible()).toEqual(true); 43 | }); 44 | it('should find yml file in home directory', () => { 45 | fs.writeFileSync(createdHomeDir + '/.atom-build.yml', fs.readFileSync(__dirname + '/fixture/.atom-build.yml')); 46 | expect(builder.isEligible()).toEqual(true); 47 | }); 48 | }); 49 | 50 | describe('when .atom-build config is on project directory', () => { 51 | it('should find json file in home directory', () => { 52 | fs.writeFileSync(directory + '/.atom-build.json', fs.readFileSync(__dirname + '/fixture/.atom-build.json')); 53 | expect(builder.isEligible()).toEqual(true); 54 | }); 55 | it('should find cson file in home directory', () => { 56 | fs.writeFileSync(directory + '/.atom-build.cson', fs.readFileSync(__dirname + '/fixture/.atom-build.cson')); 57 | expect(builder.isEligible()).toEqual(true); 58 | }); 59 | it('should find yml file in home directory', () => { 60 | fs.writeFileSync(directory + '/.atom-build.yml', fs.readFileSync(__dirname + '/fixture/.atom-build.yml')); 61 | expect(builder.isEligible()).toEqual(true); 62 | }); 63 | }); 64 | 65 | describe('when .atom-build.cson exists', () => { 66 | it('it should provide targets', () => { 67 | fs.writeFileSync(directory + '.atom-build.cson', fs.readFileSync(__dirname + '/fixture/.atom-build.cson')); 68 | expect(builder.isEligible()).toEqual(true); 69 | 70 | waitsForPromise(() => { 71 | return Promise.resolve(builder.settings()).then(settings => { 72 | const s = settings[0]; 73 | expect(s.exec).toEqual('echo'); 74 | expect(s.args).toEqual([ 'arg1', 'arg2' ]); 75 | expect(s.name).toEqual('Custom: Compose masterpiece'); 76 | expect(s.sh).toEqual(false); 77 | expect(s.cwd).toEqual('/some/directory'); 78 | expect(s.errorMatch).toEqual('(?\\w+.js):(?\\d+)'); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | describe('when .atom-build.json exists', () => { 85 | it('it should provide targets', () => { 86 | fs.writeFileSync(`${directory}.atom-build.json`, fs.readFileSync(`${__dirname}/fixture/.atom-build.json`)); 87 | expect(builder.isEligible()).toEqual(true); 88 | 89 | waitsForPromise(() => { 90 | return Promise.resolve(builder.settings()).then(settings => { 91 | const s = settings[0]; 92 | expect(s.exec).toEqual('dd'); 93 | expect(s.args).toEqual([ 'if=.atom-build.json' ]); 94 | expect(s.name).toEqual('Custom: Fly to moon'); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('when .atom-build.yml exists', () => { 101 | it('it should provide targets', () => { 102 | fs.writeFileSync(`${directory}.atom-build.yml`, fs.readFileSync(`${__dirname}/fixture/.atom-build.yml`)); 103 | expect(builder.isEligible()).toEqual(true); 104 | 105 | waitsForPromise(() => { 106 | return Promise.resolve(builder.settings()).then(settings => { 107 | const s = settings[0]; 108 | expect(s.exec).toEqual('echo'); 109 | expect(s.args).toEqual([ 'hello', 'world', 'from', 'yaml' ]); 110 | expect(s.name).toEqual('Custom: yaml conf'); 111 | }); 112 | }); 113 | }); 114 | }); 115 | 116 | describe('when .atom-build.yaml exists', () => { 117 | it('it should provide targets', () => { 118 | fs.writeFileSync(`${directory}.atom-build.yaml`, fs.readFileSync(`${__dirname}/fixture/.atom-build.yml`)); 119 | expect(builder.isEligible()).toEqual(true); 120 | 121 | waitsForPromise(() => { 122 | return Promise.resolve(builder.settings()).then(settings => { 123 | const s = settings[0]; 124 | expect(s.exec).toEqual('echo'); 125 | expect(s.args).toEqual([ 'hello', 'world', 'from', 'yaml' ]); 126 | expect(s.name).toEqual('Custom: yaml conf'); 127 | }); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('when .atom-build.js exists', () => { 133 | it('it should provide targets', () => { 134 | fs.writeFileSync(`${directory}.atom-build.js`, fs.readFileSync(`${__dirname}/fixture/.atom-build.js`)); 135 | expect(builder.isEligible()).toEqual(true); 136 | 137 | waitsForPromise(() => { 138 | return Promise.resolve(builder.settings()).then(settings => { 139 | const s = settings[0]; 140 | expect(s.exec).toEqual('echo'); 141 | expect(s.args).toEqual([ 'hello', 'world', 'from', 'js' ]); 142 | expect(s.name).toEqual('Custom: from js'); 143 | }); 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.cson: -------------------------------------------------------------------------------- 1 | cmd: "echo" 2 | args: ["arg1", "arg2"] 3 | cwd: "/some/directory" 4 | sh: false 5 | name: "Compose masterpiece" 6 | errorMatch: "(?\\w+.js):(?\\d+)" 7 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-function.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'cake' ], 4 | name: 'from js', 5 | sh: true, 6 | functionMatch: function (terminal_output) { 7 | return [ 8 | { 9 | file: '.atom-build.js', 10 | line: '1', 11 | col: '5', 12 | }, 13 | { 14 | file: '.atom-build.js', 15 | line: '2', 16 | }, 17 | { 18 | file: '.atom-build.js', 19 | line: '5', 20 | } 21 | ]; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-long-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo 1 && echo 2 && echo 3 && echo 4 && echo 5 && echo 6 && echo file:$FILE,line:3,column:8 && echo 1 && echo 2 && echo 3 && echo 4 && echo 5 && echo file:$FILE,line:2,column:5 && echo 1 && echo 2 && echo 3 && echo 4 && echo 5 && echo 6 && exit 1", 3 | "env": { 4 | "FILE": ".atom-build.json" 5 | }, 6 | "errorMatch": "file:(?[^$,]+),line:(?\\d+),column:(?\\d+)" 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-multiple-errorMatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo file:.atom-build.json,line:3,column:8 && echo file:.atom-build.json,line:1,column::::2 && echo file:.atom-build.json,line:2,column:5 && exit 1", 3 | "errorMatch": [ 4 | "file:(?[^,]+),line:(?\\d+),column:(?\\d+)", 5 | "file:(?[^,]+),line:(?\\d+),column::::(?\\d+)" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-multiple-first.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo file:.atom-build.json,line:3,column:8 && echo file:.atom-build.json,line:2,column:5 && echo file:.atom-build.json,line:1,column:1 && exit 1", 3 | "errorMatch": "file:(?[^,]+),line:(?\\d+),column:(?\\d+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo file:.atom-build.json,line:3,column:8 && echo file:.atom-build.json,line:2,column:5 && exit 1", 3 | "errorMatch": "file:(?[^,]+),line:(?\\d+),column:(?\\d+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-no-exit1.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo 'file:.atom-build.json,line:3,column:8' && exit 0", 3 | "errorMatch": "file:(?[^,]+),line:(?\\d+),column:(?\\d+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-no-file.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo 'file:black-hole,line:3,column:8' && exit 1", 3 | "errorMatch": "file:(?[^,]+),line:(?\\d+),column:(?\\d+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match-no-line-col.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo 'file:.atom-build.json,line:3,column:8' && exit 1", 3 | "errorMatch": "file:(?[^,]+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo 'file:.atom-build.json,line:3,column:8' && exit 1", 3 | "errorMatch": "file:(?[^,]+),line:(?\\d+),column:(?\\d+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.error-match.message.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo file:.atom-build.json,line:3,column:8: very bad things&& exit 1", 3 | "errorMatch": "file:(?[^,]+),line:(?\\d+),column:(?\\d+):\\s+(?.+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'hello', 'world', 'from', 'js' ], 4 | name: 'from js' 5 | }; 6 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "dd", 3 | "args": [ "if=.atom-build.json" ], 4 | "name": "Fly to moon" 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.match-function-change-dirs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'cat', 3 | args: [ 'change_dir_output.txt' ], 4 | name: 'change dir', 5 | sh: true, 6 | functionMatch: function (output) { 7 | const enterDir = /^make\[\d+\]: Entering directory '([^']+)'$/; 8 | const error = /^([^:]+):(\d+):(\d+): error: (.+)$/; 9 | const array = []; 10 | var dir = null; 11 | output.split(/\r?\n/).forEach(line => { 12 | const dir_match = enterDir.exec(line); 13 | if (dir_match) { 14 | dir = dir_match[1]; 15 | } else { 16 | const error_match = error.exec(line); 17 | if (error_match) { 18 | array.push({ 19 | file: dir ? dir + '/' + error_match[1] : error_match[1], 20 | line: error_match[2], 21 | col: error_match[3], 22 | message: error_match[4], 23 | }); 24 | } 25 | } 26 | }); 27 | return array; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.match-function-html.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'doughnut' ], 4 | name: 'from js', 5 | sh: true, 6 | functionMatch: function (terminal_output) { 7 | return [ 8 | { 9 | file: '.atom-build.js', 10 | line: '5', 11 | type: 'Warning', 12 | html_message: 'mildly bad things', 13 | } 14 | ]; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.match-function-message-and-html.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'pancake' ], 4 | name: 'from js', 5 | sh: true, 6 | functionMatch: function (terminal_output) { 7 | return [ 8 | { 9 | file: '.atom-build.js', 10 | line: '5', 11 | type: 'Warning', 12 | message: 'something happened in plain text', 13 | html_message: 'something happened in html', 14 | } 15 | ]; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.match-function-trace-html.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'cake' ], 4 | name: 'from js', 5 | sh: true, 6 | functionMatch: function (terminal_output) { 7 | return [ 8 | { 9 | file: '.atom-build.js', 10 | line: '6', 11 | type: 'Error', 12 | trace: [ 13 | { 14 | type: 'Explanation', 15 | html_message: 'insert great explanation here', 16 | } 17 | ], 18 | } 19 | ]; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.match-function-trace-message-and-html.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'pancake' ], 4 | name: 'from js', 5 | sh: true, 6 | functionMatch: function (terminal_output) { 7 | return [ 8 | { 9 | file: '.atom-build.js', 10 | line: '6', 11 | type: 'Error', 12 | trace: [ 13 | { 14 | type: 'Explanation', 15 | message: 'insert plain text explanation here', 16 | html_message: 'insert html explanation here', 17 | } 18 | ], 19 | } 20 | ]; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.match-function-trace.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'cake' ], 4 | name: 'from js', 5 | sh: true, 6 | functionMatch: function (terminal_output) { 7 | return [ 8 | { 9 | file: '.atom-build.js', 10 | line: '6', 11 | type: 'Error', 12 | trace: [ 13 | { 14 | type: 'Explanation', 15 | message: 'insert great explanation here', 16 | } 17 | ], 18 | } 19 | ]; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.match-function-warning.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cmd: 'echo', 3 | args: [ 'cake' ], 4 | name: 'from js', 5 | sh: true, 6 | functionMatch: function (terminal_output) { 7 | return [ 8 | { 9 | file: '.atom-build.js', 10 | line: '5', 11 | type: 'Warning', 12 | message: 'mildly bad things', 13 | } 14 | ]; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.replace.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo", 3 | "args": [ 4 | "FILE_ACTIVE={FILE_ACTIVE}", 5 | "PROJECT_PATH={PROJECT_PATH}", 6 | "REPO_BRANCH_SHORT={REPO_BRANCH_SHORT}", 7 | "FILE_ACTIVE_NAME={FILE_ACTIVE_NAME}", 8 | "FILE_ACTIVE_NAME_BASE={FILE_ACTIVE_NAME_BASE}", 9 | "FILE_ACTIVE_CURSOR_ROW={FILE_ACTIVE_CURSOR_ROW}", 10 | "FILE_ACTIVE_CURSOR_COLUMN={FILE_ACTIVE_CURSOR_COLUMN}", 11 | "SELECTION={SELECTION}", 12 | "FROM_ENV=$FROM_ENV", 13 | "FROM_PROCESS_ENV=$FROM_PROCESS_ENV" 14 | ], 15 | "env": { 16 | "FROM_ENV": "{FILE_ACTIVE}" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.sh-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo", 3 | "args": ["Hello"], 4 | "env": {} 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.sh-false.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo", 3 | "args": ["Hello"], 4 | "sh": false, 5 | "env": {} 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.sh-true.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo", 3 | "args": ["Hello"], 4 | "sh": true, 5 | "env": {} 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.shell.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo $SOMEVAR", 3 | "args": [], 4 | "env": { 5 | "SOMEVAR": "Good news, everyone!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.syntax-error.json: -------------------------------------------------------------------------------- 1 | { 2 | the first t in this sentance is an unexpected token: "at least if parsed as json" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The default build", 3 | "cmd": "echo default", 4 | "targets": { 5 | "Some customized build": { 6 | "cmd": "echo customized" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.warning-match.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": "echo file:.atom-build.json,line:3,column:8 && echo file:.atom-build.json,line:2,column:5 && exit 0", 3 | "warningMatch": "file:(?[^,]+),line:(?\\d+),column:(?\\d+)" 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/.atom-build.yml: -------------------------------------------------------------------------------- 1 | cmd: echo 2 | args: 3 | - hello 4 | - world 5 | - from 6 | - yaml 7 | name: yaml conf 8 | -------------------------------------------------------------------------------- /spec/fixture/atom-build-hooks-dummy-package/main.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | const hooks = { 4 | preBuild: () => {}, 5 | postBuild: () => {} 6 | }; 7 | 8 | class Builder { 9 | getNiceName() { 10 | return 'Build with hooks'; 11 | } 12 | 13 | isEligible() { 14 | return true; 15 | } 16 | 17 | settings() { 18 | return [ 19 | { 20 | exec: 'exit', 21 | args: ['0'], 22 | atomCommandName: 'build:hook-test:succeeding', 23 | preBuild: () => hooks.preBuild(), 24 | postBuild: (success) => hooks.postBuild(success) 25 | }, 26 | { 27 | exec: 'exit', 28 | args: ['1'], 29 | atomCommandName: 'build:hook-test:failing', 30 | preBuild: () => hooks.preBuild(), 31 | postBuild: (success) => hooks.postBuild(success) 32 | } 33 | ]; 34 | } 35 | } 36 | 37 | module.exports = { 38 | activate: () => {}, 39 | provideBuilder: () => Builder, 40 | hooks: hooks 41 | }; 42 | -------------------------------------------------------------------------------- /spec/fixture/atom-build-hooks-dummy-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-build-hooks-dummy-package", 3 | "main": "main", 4 | "providedServices": { 5 | "builder": { 6 | "versions": { 7 | "2.0.0": "provideBuilder" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixture/atom-build-spec-linter/atom-build-spec-linter.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | class Linter { 4 | constructor() { 5 | this.messages = []; 6 | } 7 | dispose() {} 8 | setMessages(msg) { 9 | this.messages = this.messages.concat(msg); 10 | } 11 | deleteMessages() { 12 | this.messages = []; 13 | } 14 | } 15 | 16 | module.exports = { 17 | activate: () => {}, 18 | provideIndie: () => ({ 19 | register: (obj) => { 20 | this.registered = obj; 21 | this.linter = new Linter(); 22 | return this.linter; 23 | } 24 | }), 25 | 26 | hasRegistered: () => { 27 | return this.registered !== undefined; 28 | }, 29 | 30 | getLinter: () => { 31 | return this.linter; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /spec/fixture/atom-build-spec-linter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-build-spec-linter", 3 | "main": "atom-build-spec-linter", 4 | "providedServices": { 5 | "linter-indie": { 6 | "versions": { 7 | "1.0.0": "provideIndie" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixture/change_dir_output.txt: -------------------------------------------------------------------------------- 1 | make all-recursive 2 | make[1]: Entering directory 'foo' 3 | Making all in src 4 | make[2]: Entering directory 'foo/src' 5 | CC testmake.o 6 | testmake.c: In function 'main': 7 | testmake.c:3:5: error: unknown type name 'error' 8 | error is here! 9 | ^ 10 | -------------------------------------------------------------------------------- /spec/helpers.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | export const isWin = process.platform === 'win32'; 4 | export const sleep = (duration) => isWin ? `ping 127.0.0.1 -n ${duration} > NUL` : `sleep ${duration}`; 5 | export const cat = () => isWin ? 'type' : 'cat'; 6 | export const shellCmd = isWin ? 'cmd /C' : '/bin/sh -c'; 7 | export const waitTime = process.env.CI ? 2400 : 200; 8 | -------------------------------------------------------------------------------- /spec/linter-intergration-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import os from 'os'; 4 | import fs from 'fs-extra'; 5 | import temp from 'temp'; 6 | import specHelpers from 'atom-build-spec-helpers'; 7 | import { sleep } from './helpers'; 8 | 9 | describe('Linter Integration', () => { 10 | let directory = null; 11 | let workspaceElement = null; 12 | let dummyPackage = null; 13 | const join = require('path').join; 14 | const originalHomedirFn = os.homedir; 15 | 16 | temp.track(); 17 | 18 | beforeEach(() => { 19 | const createdHomeDir = temp.mkdirSync('atom-build-spec-home'); 20 | os.homedir = () => createdHomeDir; 21 | directory = fs.realpathSync(temp.mkdirSync({ prefix: 'atom-build-spec-' })); 22 | atom.project.setPaths([ directory ]); 23 | 24 | atom.config.set('build.buildOnSave', false); 25 | atom.config.set('build.panelVisibility', 'Toggle'); 26 | atom.config.set('build.saveOnBuild', false); 27 | atom.config.set('build.scrollOnError', false); 28 | atom.config.set('build.notificationOnRefresh', true); 29 | atom.config.set('editor.fontSize', 14); 30 | 31 | jasmine.unspy(window, 'setTimeout'); 32 | jasmine.unspy(window, 'clearTimeout'); 33 | 34 | runs(() => { 35 | workspaceElement = atom.views.getView(atom.workspace); 36 | jasmine.attachToDOM(workspaceElement); 37 | }); 38 | 39 | waitsForPromise(() => { 40 | return Promise.resolve() 41 | .then(() => atom.packages.activatePackage('build')) 42 | .then(() => atom.packages.activatePackage(join(__dirname, 'fixture', 'atom-build-spec-linter'))) 43 | .then(() => (dummyPackage = atom.packages.getActivePackage('atom-build-spec-linter').mainModule)); 44 | }); 45 | }); 46 | 47 | afterEach(() => { 48 | os.homedir = originalHomedirFn; 49 | try { fs.removeSync(directory); } catch (e) { console.warn('Failed to clean up: ', e); } 50 | }); 51 | 52 | describe('when error matching and linter is activated', () => { 53 | it('should push those errors to the linter', () => { 54 | expect(dummyPackage.hasRegistered()).toEqual(true); 55 | fs.writeFileSync(join(directory, '.atom-build.json'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.error-match-multiple.json'))); 56 | 57 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 58 | 59 | waitsFor(() => { 60 | return workspaceElement.querySelector('.build .title') && 61 | workspaceElement.querySelector('.build .title').classList.contains('error'); 62 | }); 63 | 64 | runs(() => { 65 | const linter = dummyPackage.getLinter(); 66 | expect(linter.messages).toEqual([ 67 | { 68 | filePath: join(directory, '.atom-build.json'), 69 | range: [ [2, 7], [2, 7] ], 70 | text: 'Error from build', 71 | html: undefined, 72 | type: 'Error', 73 | severity: 'error', 74 | trace: undefined 75 | }, 76 | { 77 | filePath: join(directory, '.atom-build.json'), 78 | range: [ [1, 4], [1, 4] ], 79 | text: 'Error from build', 80 | html: undefined, 81 | type: 'Error', 82 | severity: 'error', 83 | trace: undefined 84 | } 85 | ]); 86 | }); 87 | }); 88 | 89 | it('should parse `message` and include that to linter', () => { 90 | expect(dummyPackage.hasRegistered()).toEqual(true); 91 | fs.writeFileSync(join(directory, '.atom-build.json'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.error-match.message.json'))); 92 | 93 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 94 | 95 | waitsFor(() => { 96 | return workspaceElement.querySelector('.build .title') && 97 | workspaceElement.querySelector('.build .title').classList.contains('error'); 98 | }); 99 | 100 | runs(() => { 101 | const linter = dummyPackage.getLinter(); 102 | expect(linter.messages).toEqual([ 103 | { 104 | filePath: join(directory, '.atom-build.json'), 105 | range: [ [2, 7], [2, 7] ], 106 | text: 'very bad things', 107 | html: undefined, 108 | type: 'Error', 109 | severity: 'error', 110 | trace: undefined 111 | } 112 | ]); 113 | }); 114 | }); 115 | 116 | it('should emit warnings just like errors', () => { 117 | expect(dummyPackage.hasRegistered()).toEqual(true); 118 | fs.writeFileSync(join(directory, '.atom-build.js'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.match-function-warning.js'))); 119 | 120 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 121 | 122 | waitsFor(() => { 123 | return workspaceElement.querySelector('.build .title') && 124 | workspaceElement.querySelector('.build .title').classList.contains('success'); 125 | }); 126 | 127 | runs(() => { 128 | const linter = dummyPackage.getLinter(); 129 | expect(linter.messages).toEqual([ 130 | { 131 | filePath: join(directory, '.atom-build.js'), 132 | range: [ [4, 0], [4, 0] ], 133 | text: 'mildly bad things', 134 | html: undefined, 135 | type: 'Warning', 136 | severity: 'warning', 137 | trace: undefined 138 | } 139 | ]); 140 | }); 141 | }); 142 | 143 | it('should attach traces to matches where applicable', () => { 144 | expect(dummyPackage.hasRegistered()).toEqual(true); 145 | fs.writeFileSync(join(directory, '.atom-build.js'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.match-function-trace.js'))); 146 | 147 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 148 | 149 | waitsFor(() => { 150 | return workspaceElement.querySelector('.build .title') && 151 | workspaceElement.querySelector('.build .title').classList.contains('error'); 152 | }); 153 | 154 | runs(() => { 155 | const linter = dummyPackage.getLinter(); 156 | expect(linter.messages).toEqual([ 157 | { 158 | filePath: join(directory, '.atom-build.js'), 159 | range: [ [5, 0], [5, 0] ], 160 | text: 'Error from build', 161 | html: undefined, 162 | type: 'Error', 163 | severity: 'error', 164 | trace: [ 165 | { 166 | text: 'insert great explanation here', 167 | html: undefined, 168 | severity: 'info', 169 | type: 'Explanation', 170 | range: [ [0, 0], [0, 0]], 171 | filePath: undefined 172 | } 173 | ] 174 | } 175 | ]); 176 | }); 177 | }); 178 | 179 | it('should clear linter errors when starting a new build', () => { 180 | expect(dummyPackage.hasRegistered()).toEqual(true); 181 | fs.writeFileSync(join(directory, '.atom-build.json'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.error-match.message.json'))); 182 | 183 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 184 | 185 | waitsFor(() => { 186 | return workspaceElement.querySelector('.build .title') && 187 | workspaceElement.querySelector('.build .title').classList.contains('error'); 188 | }); 189 | 190 | runs(() => { 191 | const linter = dummyPackage.getLinter(); 192 | expect(linter.messages).toEqual([ 193 | { 194 | filePath: join(directory, '.atom-build.json'), 195 | range: [ [2, 7], [2, 7] ], 196 | text: 'very bad things', 197 | html: undefined, 198 | type: 'Error', 199 | severity: 'error', 200 | trace: undefined 201 | } 202 | ]); 203 | fs.writeFileSync(join(directory, '.atom-build.json'), JSON.stringify({ 204 | cmd: `${sleep(30)}` 205 | })); 206 | }); 207 | 208 | waitsForPromise(() => specHelpers.refreshAwaitTargets()); 209 | 210 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 211 | 212 | waitsFor(() => { 213 | return workspaceElement.querySelector('.build .title') && 214 | !workspaceElement.querySelector('.build .title').classList.contains('error') && 215 | !workspaceElement.querySelector('.build .title').classList.contains('success'); 216 | }); 217 | 218 | runs(() => { 219 | expect(dummyPackage.getLinter().messages.length).toEqual(0); 220 | }); 221 | }); 222 | 223 | it('should leave text undefined if html is set', () => { 224 | expect(dummyPackage.hasRegistered()).toEqual(true); 225 | fs.writeFileSync(join(directory, '.atom-build.js'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.match-function-html.js'))); 226 | 227 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 228 | 229 | waitsFor(() => { 230 | return workspaceElement.querySelector('.build .title') && 231 | workspaceElement.querySelector('.build .title').classList.contains('success'); 232 | }); 233 | 234 | runs(() => { 235 | const linter = dummyPackage.getLinter(); 236 | expect(linter.messages).toEqual([ 237 | { 238 | filePath: join(directory, '.atom-build.js'), 239 | range: [ [4, 0], [4, 0] ], 240 | text: undefined, 241 | html: 'mildly bad things', 242 | type: 'Warning', 243 | severity: 'warning', 244 | trace: undefined 245 | } 246 | ]); 247 | }); 248 | }); 249 | 250 | it('should leave text undefined if html is set in traces', () => { 251 | expect(dummyPackage.hasRegistered()).toEqual(true); 252 | fs.writeFileSync(join(directory, '.atom-build.js'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.match-function-trace-html.js'))); 253 | 254 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 255 | 256 | waitsFor(() => { 257 | return workspaceElement.querySelector('.build .title') && 258 | workspaceElement.querySelector('.build .title').classList.contains('error'); 259 | }); 260 | 261 | runs(() => { 262 | const linter = dummyPackage.getLinter(); 263 | expect(linter.messages).toEqual([ 264 | { 265 | filePath: join(directory, '.atom-build.js'), 266 | range: [ [5, 0], [5, 0] ], 267 | text: 'Error from build', 268 | html: undefined, 269 | type: 'Error', 270 | severity: 'error', 271 | trace: [ 272 | { 273 | text: undefined, 274 | html: 'insert great explanation here', 275 | severity: 'info', 276 | type: 'Explanation', 277 | range: [ [0, 0], [0, 0]], 278 | filePath: undefined 279 | } 280 | ] 281 | } 282 | ]); 283 | }); 284 | }); 285 | 286 | it('should give priority to text over html when both are set', () => { 287 | expect(dummyPackage.hasRegistered()).toEqual(true); 288 | fs.writeFileSync(join(directory, '.atom-build.js'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.match-function-message-and-html.js'))); 289 | 290 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 291 | 292 | waitsFor(() => { 293 | return workspaceElement.querySelector('.build .title') && 294 | workspaceElement.querySelector('.build .title').classList.contains('success'); 295 | }); 296 | 297 | runs(() => { 298 | const linter = dummyPackage.getLinter(); 299 | expect(linter.messages).toEqual([ 300 | { 301 | filePath: join(directory, '.atom-build.js'), 302 | range: [ [4, 0], [4, 0] ], 303 | text: 'something happened in plain text', 304 | html: undefined, 305 | type: 'Warning', 306 | severity: 'warning', 307 | trace: undefined 308 | } 309 | ]); 310 | }); 311 | }); 312 | 313 | it('should give priority to text over html when both are set in traces', () => { 314 | expect(dummyPackage.hasRegistered()).toEqual(true); 315 | fs.writeFileSync(join(directory, '.atom-build.js'), fs.readFileSync(join(__dirname, 'fixture', '.atom-build.match-function-trace-message-and-html.js'))); 316 | 317 | runs(() => atom.commands.dispatch(workspaceElement, 'build:trigger')); 318 | 319 | waitsFor(() => { 320 | return workspaceElement.querySelector('.build .title') && 321 | workspaceElement.querySelector('.build .title').classList.contains('error'); 322 | }); 323 | 324 | runs(() => { 325 | const linter = dummyPackage.getLinter(); 326 | expect(linter.messages).toEqual([ 327 | { 328 | filePath: join(directory, '.atom-build.js'), 329 | range: [ [5, 0], [5, 0] ], 330 | text: 'Error from build', 331 | html: undefined, 332 | type: 'Error', 333 | severity: 'error', 334 | trace: [ 335 | { 336 | text: 'insert plain text explanation here', 337 | html: undefined, 338 | severity: 'info', 339 | type: 'Explanation', 340 | range: [ [0, 0], [0, 0]], 341 | filePath: undefined 342 | } 343 | ] 344 | } 345 | ]); 346 | }); 347 | }); 348 | }); 349 | }); 350 | -------------------------------------------------------------------------------- /spec/utils-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { uniquifySettings, getDefaultSettings, replace } from '../lib/utils.js'; 4 | 5 | describe('utils', () => { 6 | describe('when uniquifying settings', () => { 7 | it('should append numbers on equally named settings', () => { 8 | const settings = [ 9 | { name: 'name', cwd: 'cwd1' }, 10 | { name: 'name', cwd: 'cwd2' }, 11 | { name: 'name', cwd: 'cwd3' }, 12 | { name: 'name', cwd: 'cwd4' } 13 | ]; 14 | expect(uniquifySettings(settings)).toEqual([ 15 | { name: 'name', cwd: 'cwd1' }, 16 | { name: 'name - 1', cwd: 'cwd2' }, 17 | { name: 'name - 2', cwd: 'cwd3' }, 18 | { name: 'name - 3', cwd: 'cwd4' } 19 | ]); 20 | }); 21 | 22 | it('should append numbers on equally named settings, but leave unique names untouched', () => { 23 | const settings = [ 24 | { name: 'name', cwd: 'cwd1' }, 25 | { name: 'name', cwd: 'cwd2' }, 26 | { name: 'otherName', cwd: 'cwd3' }, 27 | { name: 'yetAnotherName', cwd: 'cwd4' } 28 | ]; 29 | expect(uniquifySettings(settings)).toEqual([ 30 | { name: 'name', cwd: 'cwd1' }, 31 | { name: 'name - 1', cwd: 'cwd2' }, 32 | { name: 'otherName', cwd: 'cwd3' }, 33 | { name: 'yetAnotherName', cwd: 'cwd4' } 34 | ]); 35 | }); 36 | }); 37 | 38 | describe('when getting default settings', () => { 39 | it('should prioritize specified settings', () => { 40 | expect(getDefaultSettings('/cwd', { cmd: 'echo hello', cwd: 'relative' })).toEqual({ 41 | cmd: 'echo hello', 42 | cwd: 'relative', 43 | args: [], 44 | env: {}, 45 | sh: true, 46 | errorMatch: '' 47 | }); 48 | }); 49 | 50 | it('should be possible to override any argument', () => { 51 | expect(getDefaultSettings('/cwd', { 52 | cmd: 'echo hello', 53 | cwd: 'relative', 54 | args: [ 'arg1' ], 55 | env: { 'key1': 'val1' }, 56 | sh: false, 57 | errorMatch: '^regex$' 58 | })).toEqual({ 59 | cmd: 'echo hello', 60 | cwd: 'relative', 61 | args: [ 'arg1' ], 62 | env: { 'key1': 'val1' }, 63 | sh: false, 64 | errorMatch: '^regex$' 65 | }); 66 | }); 67 | 68 | it('should take the specifed cwd if omitted from settings', () => { 69 | expect(getDefaultSettings('/cwd', { cmd: 'make' })).toEqual({ 70 | cmd: 'make', 71 | cwd: '/cwd', 72 | args: [], 73 | env: {}, 74 | sh: true, 75 | errorMatch: '' 76 | }); 77 | }); 78 | 79 | it('should not replace values which are not strings', () => { 80 | expect(replace(12)).toEqual(12); 81 | expect(replace({a: '{FILE_ACTIVE}' })).toEqual({a: '{FILE_ACTIVE}'}); 82 | expect(replace([ 1, 2, 3 ])).toEqual([ 1, 2, 3 ]); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /styles/animations.less: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes spin { 2 | 0% { 3 | transform: rotate(0deg); 4 | } 5 | 100% { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | .spin { 11 | animation: spin 2.5s linear infinite; 12 | } 13 | 14 | @-webkit-keyframes flash { 15 | 0% { 16 | opacity: 0.2; 17 | } 18 | 50% { 19 | opacity: 1.0; 20 | } 21 | 100% { 22 | opacity: 0.2; 23 | } 24 | } 25 | 26 | @-webkit-keyframes flash-background-success { 27 | 0% { 28 | background-color: rgba( 29 | red(@background-color-success), 30 | green(@background-color-success), 31 | blue(@background-color-success), 32 | 0 33 | ); 34 | background-image: -webkit-linear-gradient( 35 | rgba( 36 | red(@background-color-success), 37 | green(@background-color-success), 38 | blue(@background-color-success), 39 | 0 40 | ), 41 | rgba( 42 | red(@background-color-success), 43 | green(@background-color-success), 44 | blue(@background-color-success), 45 | 0.1 46 | ) 47 | ); 48 | } 49 | 20% { 50 | background-color: rgba( 51 | red(@background-color-success), 52 | green(@background-color-success), 53 | blue(@background-color-success), 54 | 0.3 55 | ); 56 | background-image: -webkit-linear-gradient( 57 | rgba( 58 | red(@background-color-success), 59 | green(@background-color-success), 60 | blue(@background-color-success), 61 | 0.3 62 | ), 63 | rgba( 64 | red(@background-color-success), 65 | green(@background-color-success), 66 | blue(@background-color-success), 67 | 0.1 68 | ) 69 | ); 70 | } 71 | 80% { 72 | background-color: rgba( 73 | red(@background-color-success), 74 | green(@background-color-success), 75 | blue(@background-color-success), 76 | 0.3 77 | ); 78 | background-image: -webkit-linear-gradient( 79 | rgba( 80 | red(@background-color-success), 81 | green(@background-color-success), 82 | blue(@background-color-success), 83 | 0.3 84 | ), 85 | rgba( 86 | red(@background-color-success), 87 | green(@background-color-success), 88 | blue(@background-color-success), 89 | 0.1 90 | ) 91 | ); 92 | } 93 | 100% { 94 | background-color: rgba( 95 | red(@background-color-success), 96 | green(@background-color-success), 97 | blue(@background-color-success), 98 | 0 99 | ); 100 | background-image: -webkit-linear-gradient( 101 | rgba( 102 | red(@background-color-success), 103 | green(@background-color-success), 104 | blue(@background-color-success), 105 | 0 106 | ), 107 | rgba( 108 | red(@background-color-success), 109 | green(@background-color-success), 110 | blue(@background-color-success), 111 | 0.1 112 | ) 113 | ); 114 | } 115 | } 116 | 117 | @-webkit-keyframes flash-background-error { 118 | 0% { 119 | background-color: rgba( 120 | red(@background-color-error), 121 | green(@background-color-error), 122 | blue(@background-color-error), 123 | 0 124 | ); 125 | background-image: -webkit-linear-gradient( 126 | rgba( 127 | red(@background-color-error), 128 | green(@background-color-error), 129 | blue(@background-color-error), 130 | 0 131 | ), 132 | rgba( 133 | red(@background-color-error), 134 | green(@background-color-error), 135 | blue(@background-color-error), 136 | 0.1 137 | ) 138 | ); 139 | } 140 | 20% { 141 | background-color: rgba( 142 | red(@background-color-error), 143 | green(@background-color-error), 144 | blue(@background-color-error), 145 | 0.3 146 | ); 147 | background-image: -webkit-linear-gradient( 148 | rgba( 149 | red(@background-color-error), 150 | green(@background-color-error), 151 | blue(@background-color-error), 152 | 0.3 153 | ), 154 | rgba( 155 | red(@background-color-error), 156 | green(@background-color-error), 157 | blue(@background-color-error), 158 | 0.1 159 | ) 160 | ); 161 | } 162 | 80% { 163 | background-color: rgba( 164 | red(@background-color-error), 165 | green(@background-color-error), 166 | blue(@background-color-error), 167 | 0.3 168 | ); 169 | background-image: -webkit-linear-gradient( 170 | rgba( 171 | red(@background-color-error), 172 | green(@background-color-error), 173 | blue(@background-color-error), 174 | 0.3 175 | ), 176 | rgba( 177 | red(@background-color-error), 178 | green(@background-color-error), 179 | blue(@background-color-error), 180 | 0.1 181 | ) 182 | ); 183 | } 184 | 100% { 185 | background-color: rgba( 186 | red(@background-color-error), 187 | green(@background-color-error), 188 | blue(@background-color-error), 189 | 0 190 | ); 191 | background-image: -webkit-linear-gradient( 192 | rgba( 193 | red(@background-color-error), 194 | green(@background-color-error), 195 | blue(@background-color-error), 196 | 0 197 | ), 198 | rgba( 199 | red(@background-color-error), 200 | green(@background-color-error), 201 | blue(@background-color-error), 202 | 0.1 203 | ) 204 | ); 205 | } 206 | } 207 | .spin-flash { 208 | animation: flash 0.5s linear infinite, spin 2.5s linear infinite; 209 | } 210 | -------------------------------------------------------------------------------- /styles/build.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | @import "animations"; 3 | 4 | .build { 5 | 6 | min-width: 250px; 7 | font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace; 8 | font-weight: 600; 9 | font-size: @font-size; 10 | 11 | .resizer { 12 | opacity: 0.0; 13 | position: absolute; 14 | border: solid 4px @base-border-color; 15 | } 16 | 17 | .heading { 18 | -webkit-user-select: none; 19 | line-height: 3.0em; 20 | padding: 0 3px; 21 | 22 | &.success { 23 | animation: flash-background-success linear 1.2s; 24 | } 25 | 26 | &.error { 27 | animation: flash-background-error linear 1.2s; 28 | } 29 | 30 | .heading-text { 31 | overflow: hidden; 32 | white-space: nowrap; 33 | text-overflow: ellipsis; 34 | 35 | &::before { 36 | margin-right: 5px; 37 | color: @text-color-error; 38 | animation: flash linear infinite 1.0s; 39 | } 40 | } 41 | 42 | .control-container { 43 | float: right; 44 | margin-left: @component-padding; 45 | 46 | .btn { 47 | color: @text-color-info; 48 | margin: 0 1px; 49 | } 50 | 51 | .title { 52 | float: left; 53 | 54 | .build-timer { 55 | margin-right: @component-padding; 56 | } 57 | 58 | &.error { 59 | color: @text-color-error; 60 | } 61 | 62 | &.success { 63 | color: @text-color-success; 64 | } 65 | } 66 | } 67 | } 68 | 69 | .output { 70 | &.override-theme { 71 | color: #fff; 72 | background-color: #000; 73 | } 74 | 75 | .terminal { 76 | padding: 0; 77 | margin: 0; 78 | height: 100%; 79 | width: 100%; 80 | font-weight: normal; 81 | > div { 82 | white-space: nowrap; 83 | font-size: inherit; 84 | } 85 | } 86 | 87 | .terminal-test { 88 | background-color:none; 89 | position: absolute; 90 | float: left; 91 | visibility: hidden; 92 | height: auto !important; 93 | width: auto !important; 94 | white-space: nowrap; 95 | border: none; 96 | } 97 | } 98 | } 99 | 100 | .opaque-hover { 101 | transition: opacity 0.2s linear; 102 | opacity: 0.6; 103 | &:hover { 104 | opacity: 1.0; 105 | } 106 | } 107 | 108 | #build-status-bar { 109 | &.status-success > a { 110 | color: @text-color-success; 111 | } 112 | 113 | &.status-error > a { 114 | color: @text-color-error; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/panel-left-right.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | atom-panel.left .build, 4 | atom-panel.right .build { 5 | width: 250px; 6 | display: flex; 7 | flex-direction: column; 8 | .output { 9 | flex-grow: 1; 10 | } 11 | 12 | .resizer { 13 | cursor: col-resize; 14 | height: 100%; 15 | top: 0; 16 | } 17 | } 18 | 19 | atom-panel.left .build { 20 | .resizer { 21 | right: 0; 22 | } 23 | } 24 | 25 | atom-panel.right .build { 26 | .resizer { 27 | left: 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /styles/panel-top-bottom.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | atom-panel.top .build, 4 | atom-panel.bottom .build { 5 | .resizer { 6 | cursor: row-resize; 7 | width: 100%; 8 | } 9 | 10 | .terminal { 11 | height: 150px; 12 | } 13 | } 14 | 15 | atom-panel.top .build { 16 | .resizer { 17 | bottom: 0; 18 | } 19 | } 20 | 21 | atom-panel.bottom .build { 22 | .resizer { 23 | top: 0; 24 | } 25 | } 26 | --------------------------------------------------------------------------------