├── .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 | [](https://atom.io/packages/build)
6 | [](https://atom.io/packages/build)
7 |
8 | [](https://travis-ci.org/noseglid/atom-build)
9 | [](https://ci.appveyor.com/project/noseglid/atom-build)
10 |
11 | [](https://gitter.im/noseglid/atom-build)
12 | [](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 | 
26 |
27 | #### Automatically extract targets - here with [build-make](https://github.com/AtomBuild/atom-build-make).
28 | 
29 |
30 | #### Match errors and go directly to offending code - with [Atom Linter](https://atom.io/packages/linter).
31 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------