Native Modules
Introduction
This document was last updated on 8 October 2015 for version 1.14.6.
This document intends to fully describe how code modularity occurs in Pretty Diff. At the time of this writing there is a standard module convention in JavaScript commonly referred to as ES6 modules, but it is not supported anywhere. In order to achieve modular code directly without a build process that allows the code to execute directly on various platforms a series of conventions employed together and unique problems were discovered.
The common solution to modules is a build process. I did not wish to use a build process with Pretty Diff, because I don't want to maintain a separate process for each and every environment supported or consumed by this application. Build processes tend to be domain specific tools. For ease of testing and ease of execution the same application code should execute everywhere JavaScript can execute in exactly the same way.
Code Separation
Prior to embarking upon this adventure all application code existed in a single file, prettydiff.js. There were several conveniences to keeping all code in one file, such as
- Fix all bugs and provide nearly all enhancements within a single file.
- Find nearly any logic in the entire application using an editor's find function (CTRL+F)
- It is always easier to send people one monolithic file than many small files.
- It forced me to do line by line code reviews before publishing changes. I would keep the copies of the libraries in individual files in case anybody wanted to use them, but they did not execute as primary component of the Pretty Diff application. Keeping these libraries in sync with the corresponding logic in the prettydiff.js file forced me to rethink some of my decisions.
While having virtually all of the application logic in a single file helped me to quick extend the logic and debug complex issues, particularly issues spanning multiple libraries, it came at a cost. Here is what I was missing:
- Other developers were intimidated to jump into an application containing more than 11,000 lines of terse logic.
- A single monolithic file was not desirable to many developers who only wanted to use one of the contained libraries.
- The forced code reviews, while beneficial, were exceedingly time consuming for every release even when they weren't necessary or helpful.
- Having all application logic in one file meant quicks in one library were stuck waiting for resolution on a large enhancement in another library before the updates could be launched to production.
Separated code files have proven extremely liberating. There is flexibility where it did not exist before. While it was helpful to be able to search a large monolithic file to find a specific piece of code I have not missed this feature, since I generally know in which library to look in anyways.
Identified
Breaking apart code seems into smaller pieces seems like it would be easy, but there several problems I discovered.
- Achieving modularity across all environments without a build process means each and every module must be specified manually per environment or loaded through dynamic dependency injection equally across all environments. Manual definition of modules is not actually not a big deal if the collection of modules is small, but provides maintenance challenges across environments as the number of modules increases. Dynamic module injection should be avoided due to the added HTTP overhead in the browser.
- The only way to achieve modularity in the browser without a build process is by using a script tag per module in the HTML. This means an extra HTTP request per module. It also means there are no dependency chains since requests from the browser are flat.
- In environments that use Node, but only as a secondary tool there are no relative paths. process.cwd() always returns "/" for root. So, relative paths to modules must always be absolute paths using __dirname. The namespace must exist in the global scope and be available across all files and environments. For consistency this logic should be applied for every exports statement and every require statement related to a module file.
- In Node the modules cannot require each other directly, because this produces an infinite recursive loop. Instead modules must be assigned to a namespace upon require. I chose to assign to global because it is available from Node exactly for reasons like this.
- Because of the use of an object named global from Node, I had to create an empty object for all other environments. The secondary consequence is that all modules must be universally referenced as properties of an object named global.
- In Node modules are loaded, via require, in a synchronous fashion. Therefore the order the order in which modules are loaded is irrelevant provided they are all required in the location of the code and not executed until after all modules are required. This is not so in the browser where instead modules (just files requested from different script tags) are loaded asynchronously. If a reference is used in an early file but defined in a later file it's reference must be delayed to prevent errors using something similar to a promise.
- In achieving compliance with JSLint the reference global must be mentioned in the JSLint's global comment for every file. The modules cannot be declared as properties of this object though, so instead they should be declared by their actual name even though they will be universally referenced as properties of an object named global. While this is easily accomplished it still means polluting the global scope with one reference per module.
The Solution
To turn a module into a library a couple of things need to happen. First, each module needs to require its dependencies to the global object. If the require function is available then require the appropriate dependencies otherwise, in the case of the browser, just assign the dependency to a link name in the global object.
Secondly, each and every module needs to specify an exports point. In the case of Pretty Diff I always used a property named api for this. I recommend following the examples provided in the Pretty Diff libraries because exports are applied differently between the Node way and the requirejs way.
Summary of Results
I was able to achieve native JavaScript modules across all environments without any build process and without the ES6 module convention, but it took several mistakes in production to identify the known edge cases. Fortunately, the Pretty Diff project is consumed regularly enough in a wide distribution of tools at this time that any remaining edge cases should be little or none.
Because Pretty Diff is only using six modules all from the same domain there is no perceived difference in load time or execution time. It also helps that the Pretty Diff project is fully self-contained with its only dependency being the Ace Editor, which is still served from the same domain and only available to the browser.