Merge pull request #4161 from solgenomics/topic/TomDelDb
[sgn.git] / js / README.md
blob1d87bf223cc679273bfe79ce5bb6addea4cf67d3
1 # Javascript in SGN
3 The new JavaScript system in SGN relies on Webpack, Node, and NPM. The new system allows for the use of node modules and for ES6 module imports to be resolved and bundled into an efficient number of javascript files.
5 To install Node.js and NPM run `./install_node.sh` as root.
7 In order to hook into Catalyst, Mason, and the rest of the SGN infrastructure, Webpack has been configured in a slightly atypical manner. In a typical Webpack setup, there is a JavaScript file for each page. However, with the way our legacy code is structured, this paradigm would require a large amount of refactoring. So, instead, Webpack is configured such that modern JavaScript is transpiled into separate (independently loadable) namespaces within a multi-part library call 'jsMod'.
9 The following three sections will enumerate the different locations one can use JavaScript on the site, and how they behave.
11 ## On-Page JavaScript
12 The most obvious JavaScript on the site is directly within a `<script>` tag in a Mason file. Code here is NOT touched by any JavaScript transpilers, minifiers, or by webpack. Any JavaScript written directly into the page will be transmitted to the user as-is. This means that the author of said code MUST be careful to use only ES2015 JavaScript functionality. Some things which are inappropriate for On-Page JavaScript include arrow functions (`()=>{}`) and ES6 classes (`class ClassName{}`).
14 ## Legacy JavaScript
15 #### `legacy`
16 Legacy JavaScript is, for our purposes, all JavaScript files which are managed by the `JSAN.use("")` dependency system. This means all JavaScript files previously stored in the `js/` directory. Because of important global side-effects cause by the common use of global scope definitions in these files, it is very difficult (likely impossible) to automatically convert them to a state such that Webpack is able to properly handle their interdependence (and the On-Page JavaScript which depends on their globally defined variables). As such, legacy code has been "quarantined" in the `js/source/legacy` folder. Any code in this folder continues to behave exactly as it would have before the addition of the Webpack system. As such, **legacy code is executed in the global scope, and adding to it should be avoided.** Legacy JavaScript is minified using a  _legacy minifier_ and like On-Page JavaScript, is not transpiled, this means that the author of said code MUST be careful to use only ES2015 JavaScript functionality. Failing to do so may break the minification step, or lead to incompatibilities with users' browsers. To include legacy JS on a page, one should use the following pattern:
18 | File Paths | Mason Pattern | 
19 | --------- | ------------- |
20 | `js/source/legacy/CXGN/Effects.js`, `js/source/legacy/CXGN.Phenome/Locus.js`, `js/source/legacy/MochiKit/DOM.js` | `<& /util/import_javascript, legacy => [ "CXGN.Effects", "CXGN.Phenome.Locus", "MochiKit.DOM" ] &>` |
23 ## Modern JavaScript
24 Modern JavaScript is defined in this documentation as source for the webpack pre-comiler. Modern JavaScript is transpiled and polyfilled to allow for the use of newer JavaScript features without worrying as much about reverse compatibility. Having added a transpilation step, we can take advantage of this existing overhead by also using Webpack to resolve and bundle dependencies. This allows us to use ES6 module imports and exports. Because webpack relies on an "Entry" model, we have two main folders of Modern JavaScript files. 
26 #### `entries`
28 The first folder is `js/source/entries`. This contains a JS module which describes a namespace of the `jsMod` global object.
30 For example:
31 ```js
32 // js/source/entries/example.js
33 var someVariableName = "someValue";
34 export someVariableName;
35 ```
36 ```html
37 <!-- mason/**/example.mas -->
38 <& /import_javascript, entries => ["example.js"]&>
39 <script type="text/javascript">
40   // Writes to console:
41   console.log(someVariableName===undefined);
42   // -> true
43   console.log(jsMod['example'].someVariableName);
44   // -> "someValue"
45 </script>
47 ```
49 #### `modules`
51 The next is `js/source/modules`. This contains a JS module which is not exposed via jsMod, but whose code can be imported by multiple `entries`. `modules` differ from `entires` in that, if a `entries` file imports another `entries` file, the included entry will be sent to the user as a separate file. If a `entries` file imports a `modules` file, however, that module will be bundled into the same file as the entry when being sent to the user. Further, if some set of `modules` files are commonly imported by multiple `entries`, they will be bundled together as a file such that they might be cached for later use by the user's browser. [Click here for more information on that process.](https://webpack.js.org/guides/code-splitting/) Remember, `modules` files are not exposed via the `jsMod` object.  
53 For example:
54 ```js
55 // js/source/modules/example0.js
56 var myVar = "aValue";
57 export myVar;
58 ```
59 ```js
60 // js/source/entries/example1.js
61 import {myVar} from '../modules/example0.js';
62 var yetAnother = "someValue";
63 export yetAnother;
64 export var someVar = myVar;
65 ```
66 ```html
67 <!-- mason/**/example.mas -->
68 <& /import_javascript, entries => ["example1.js"]&>
69 <script type="text/javascript">
70   // Writes to console:
71   console.log(myVar===undefined);
72   // -> true
73   console.log(yetAnother===undefined);
74   // -> true
75   console.log(jsMod['example0']===undefined);
76   // -> true
77   console.log(jsMod['example1'].yetAnother);
78   // -> "someValue"
79   console.log(jsMod['example1'].myVar===undefined);
80   // -> true
81   console.log(jsMod['example1'].someVar);
82   // -> "aValue"
83 </script>
85 ```
87 ## Combining Legacy and Modern JS
89 Legacy JS cannot import or depend on Modern JS and is always executed first. Modern JS can import legacy code for global effects (aka side-effects) by specifying the relative path to the file (e.g. `import "../legacy/CXGN/Phenome/Locus.js";`). JSAN dependencies declared in legacy code _will_ be resolved. 
91 `/util/import_javascript` can also import legacy code and entry modules in one statement. `<& /import_javascript, entries => [], legacy => [] &>`
93 ## JavaScript Testing
95 #### `tests`
97 Tests are run via `node js/run-tests.js`. This script outputs TAP in stdout and other test script JS console output as stderr. Each file in the `js/test` directory is run in a separate virtual DOM. The tests are run using [jsdom](https://github.com/jsdom/jsdom), [tape](https://github.com/substack/tape), and [nock](https://github.com/nock/nock). 
99 #### [jsdom](https://github.com/jsdom/jsdom)
100 Provides the virtual DOM that tests run within. This enables tests to act as though they are running in a browser, without requiring the overhead of a system like selenium. However, there is no _rendering_– only the DOM is managed. Typical global browser variables such as `window` and `browser` are available. If you are only adding tests, you shouldn't need to interact with JSDOM functionality in any direct way.
102 #### [tape](https://github.com/substack/tape)
103 The test harness used is [tape](https://github.com/substack/tape). It was chosen due to its extraordinary flexibility. The [tape documentation](https://github.com/substack/tape) goes over much of the functionality. 
105 #### Putting tape and jsdom Together
107 - Files to be tested should be imported as one would in a source file: 
108   ```js
109   // test/example.test.js[0:1]
110   import * as Boxplotter from '../source/entries/boxplotter.js';
111   ```  
112 - In order to run tests, you can set the contents of the DOM like you might in the browser:
113   ```js
114   // test/example.test.js[1:2]
115   document.querySelector('body').innerHTML = `<div id="bxplt"></div>`;
116   ```
117 - These can then be tested using the tape harness:
118   ```js
119   // test/example.test.js[2:8]
120   test("Boxplotter", t=>{
121         t.plan(1);
122         t.doesNotThrow(()=>{
123             boxplotter = Boxplotter.init("#bxplt");
124         })
125     });
126   ```
128 #### [nock](https://github.com/nock/nock)
129 [nock](https://github.com/nock/nock) provides server mocking for testing JS with web requests. By default, test scripts cannot make web requests. You have two options to fix this. 
130 - Create a [nock interceptor](https://github.com/nock/nock#read-this---about-interceptors) like so:
131   ```js
132   var scope = nock(document.location.origin);
133   test("Test Web Request", t=>{
134       t.plan(1);
135       scope.get('/testurl').reply(200, {'yourdata':'here'});
136       fetch(document.location.origin+'/testurl')
137         .then(resp=>resp.json())
138         .then(data=>{
139           t.equals(data.yourdata,'here');
140         });
141   });
142   ```
143 - [Enable external web requests](https://github.com/nock/nock#enabling-requests) for your test file like so:
144   ```js
145   test("Test Web Request", t=>{
146       t.plan(1);
147       nock.enableNetConnect('cassavabase.org')
148       fetch('https://cassavabase.org/realurl')
149         .then(resp=>resp.json())
150         .then(data=>{
151           t.equals(data.yourdata,'here');
152         });
153   });
154   ```