JavaScript Build Process
As the product advances, we're going to be getting into fancier client-side scripting. We'll have to step up our JavaScript game, much like we've done for our CSS, to make things all-around easier to develop and maintain. Similar to our CSS, we need a build process for our JavaScript.
JavaScript is notorious for its half-baked object-oriented approach. Anything we can do to simplify development with it and make its code easier to understand will be beneficial for everyone. I've put together a very basic example of a proposed build process and attached it to this post.
What it does
- Verifies code style (JSCS)
- Verifies code quality (JSHINT)
- Compiles/transpiles the code (Babel)
- Minifies the result (UglifyJS2)
The source files in the build process carry a .es file extension and are intended to be written in ECMAScript 2015 (ES2015).
How to use it
- If you haven't already, install Node.js and NPM (probably easiest when done with brew).
- Install Grunt.
- Extract the attached archive.
- From the new project folder, run
npm install. - To build the project, run
npm run buildor justgrunt. - If all went well, you should see a "Done, without errors" message. When opening the included index.htm file, you should see an alert message and a message logged to your browser's developer console.
- Start learning ES2015.
Comments
-
This is a great start. What do you think of being able to wrap stuff in the protection-style code or allow us to combine stuff with require-js' r command line tool?
0 -
Does this mean we're going to be able to support things like page updates without a page refresh or not having to click on a save button with inputing settings in the Dashnoard?
0 -
@Lvez said:
Does this mean we're going to be able to support things like page updates without a page refresh or not having to click on a save button with inputing settings in the Dashnoard?This is step one of many to get there, but yes, we're moving in that direction. I expect you'll be hearing a bunch about ReactJS in the coming months, and that product goal is why.
0 -
@Todd said:
This is a great start. What do you think of being able to wrap stuff in the protection-style code or allow us to combine stuff with require-js' r command line tool?Limiting access to an object's members is a little convoluted in JavaScript, even with ES2015/ES6. Structuring things to restrict a variable's accessibility can be tedious, but I think it's beneficial. There's a few pure JavaScript examples of it in our Analytics scripts.
I think using the RequireJS Optimizer via r.js would be an excellent addition, once we're ready to start using RequireJS as a module loader. Until then, I think we can combine script files pretty easily in this build process by concatenating and minifying specific groups of files (although it's not as "smart" as the optimizer).
0 -
That wasn't specifically what I was talking about. If you look at most libraries they use an immediately-invoked function expression (IIFE) to protect the global scope. We actually do the same in a lot of our stuff. I was thinking we could do that in our build process, but the more research that I've done I see it's better to just do that in each individual file at the expense of a bit of size is fine.
I was looking at jQuery's build a bit and it looks like they do some regex hacks to remove extraneous IIFE's which makes me a bit nervous.
0 -
FYI: I've been doing a lot of work of the past couple of weeks with javascript building and module loading. I've discovered a pretty good build process. I'll lay out what I've found.
Grunt Babel
I was able to get a build working with a grunt babel plugin that compiles stuff into the AMD module format. This worked well alongside other libraries and I could load everything with require.js. The problem with the grunt build was that babel couldn't combine the source files and then using another task would kill the source maps which is unacceptable. Think of how hard stuff will be to debug without them. I tried a tonne of different tools and none of them seemed to work.
Using r.js
I tried r.js and it works pretty well. The problem with it is that it traces all dependencies so makes it difficult to separate vendor scripts into separate files. There is an option to exclude modules from the build, but r.js still needs them to be present while doing the build. Imagine the scenario where we have vendor scripts in core and then I want to build an addon's javascript. In this case r.js will fail. We need a build tool that will work for core and individual addons.
Exec Babel
I ended up seeing that the babel CLI had everything that I needed so I used a grunt-exec plugin and just executed the babel build instead. I also like being able to have babel options left in a separate .babelrc file. This allows people to do a quick and dirty babel build with just the command line if they want which is really nice.
Using require.js
The require.js javascript library is the original module loader. It was made for the web and was made before the explosion of node.js so doesn't rely on it. I was able to get it working much easier than of the alternatives. I tried a few loaders and researched many more. Require.js does not get much love these days, but it seems to work the best for our purposes. Still though, it bothers me to use a library during the twilight of its existence.
This is a bit of a perception thing I admit. A lot of people don't seem to realize that the main competing module standard, CommonJS, is synchronous and doesn't support asynchronous loading which we need. Most apps these days compile one giant js file with browserify or webpack and are done with it. Vanilla cannot do this. We can combine js on an addon-by-addon basis, but not everything.
Enter system.js
During my research I stumbled upon a new js module and loader format called system.js. This a standard that is currently being discussed as a core feature of javascript. Here are some of the features.
- The system.js module format supports all of the features of ES6. This means that modules transpiled with babel will load quite nicely.
- System.js modules can have cyclic dependencies.
- Modules can be loaded asynchonously in the browser or synchronously on the server.
- Loading modules asynchronously uses Promise objects which are amazing.
- Browsers should have support for this module format baked in one day. It will be pluggable support though.
There are problems with system.js though.
- The module format is very confusing. It's not meant to be hand-coded. Rather it's meant to be written in ES6 and transpiled.
- Little to no libraries out there support the system.js module format. Most libraries are coded in the Universal Module Definition (UMD) format. This format combines CommonJS, AMD, and global variables conditionally depending on what module loader it detects.
I tried out the system.js library which is supposed to load any kind of module, but couldn't get it to work. I'm sure it would have worked with a bit of tweaking, but the library is quite large and does some stuff that I don't like such as regex entire js files.
The garden-loader.js
I stumbled upon a tiny system.js micro-loader. It was small enough to read all of the code without too much effort so I tried my had and bolting on UMD support to it. The end result is the garden-loader. Here are some of the features:
- Recognize system.js modules defined with System.register(). This will be for our transpiled ES6 code.
- Recognize AMD and UMD modules defined with define(). This will be for vendor files and older modules.
- Will recognize modules that have been combined into separate files or can load modules on-demand. This will allow us to mix and match where code goes and optimize when needed.
The garden-loader is nice and small because it doesn't have a tonne of extra features. This means that we will need to program some server-side support for it to handle Vanilla's modular nature. Luckily, the addon manager has been recently rewritten and it will be fairly easy to add this support there.
1