Apologies if this idea has been floated before. I did a cursory search and couldn't find anything.
The impetus behind this proposal is a long exploration of the painful JavaScript modularity story. I'll save you the read and summarize the most salient points:
- In Bower the average JavaScript library dependency depth rounds to 0.
- Most JavaScript is not authored in the format that people consume it.
- Modularity only Just Works™ when your each item in your dependency tree has a standard way to expose itself.
These general problems also apply to other asset types (e.g. css, images), but I'll focus on JavaScript for this proposal.
Dependency Depth
@rayshan was kind enough to give me a data dump for the last 30 days of bower installs. I'm still playing around with that data, but I think I have enough to make some points. Typical dependency depth for libraries published to Bower is 0. I don't have any data on npm, but just looking into some node based projects on my computer, it's not uncommon to see dependency depths of 5 or more.
One exception to this in Bower is the clusters of libraries around foundational projects like jQuery, Ember, or Angular. Here, dependency depth is between two and three. This appears to be because these projects' communities either have a shared build ecosystem and/or a mandated modular format (ES6 for Ember, the $ namespace for jQuery, angular.module for Angular, etc).
JavaScript outside these silos tends to lack (or not expose) modularity. This lack of modularity is leading to fragmentary solutions which complicates the process of getting dependencies into projects without a lot of additional effort on the part of the package consumer.
The Gold Standard for this should be systems like Rubygems and npm where coarse (package-level) and fine-grained (specific object import) dependency is so frictionless, you'd be crazy not to use it.
To summarize:
Gets the foo package as a copy locally
var aFoo = require('foo');
Is really a shorthand for the loader to go find node_modules/foo/package.json, look for its main entry, and bring the contents of that file into the execution environment.
In Bower the process is more like:
# lots of complicated custom tooling to convert
# between module formats or wrap un-modular
# code in IIFEs for safety or wrap in module shims
require(["foo"], function(foo) {});
Problems
The lack of that common module system is forcing authors to engage in various painful-to-maintain patterns:
- Checking in compiled code (or multiple versions of it) into source control. A.k.a. the
dist/ pattern. Pain points: having a main entry in a package manifest is basically meaningless, forgetting to build before publish leads to problems where a tagged version's dist/ is still the old code, not every project uses the same folder for builds, so each consumer has to go find where code is.
- Creating special module-specific forked repos that are not associated with the main project (https://github.com/amdjs/backbone, https://github.com/amdjs/underscore) and then publishing those as new Bower packages. Problems: "This branch is 16 commits ahead, 489 commits behind jashkenas:master".
- Creating official shim repos https://github.com/components/ember. Problems: polluting Github with "read-only" projects, maintenance pains around people not knowing that the repo isn't the real one and opening issues and pull requests against it.
- The "
¯\_(ツ)_/¯" approach of shipping one module system. I chatted with @jeremyckahn about how he handles the module problem for https://github.com/jeremyckahn/rekapi/ and he basically said "what module problem?". Turns out he ships only in AMD/one global variable exposed and other format users are just not in scope. This is also true for everyone who publishes as CJS and says "just use browserify!".
- Embedding dependencies: jQuery embeds Sizzle. Ember embeds RSVP.js, router.js, route-recognizer.js, etc (about a dozen libraries). This makes combining these libraries complicated. If I build a library on top of Sizzle but not jQuery and you also pull jQuery into your project you effectively have two copies of Sizzle. You can work around this by creating custom builds (jQuery compat and non-jQuery compat) but that just adds to the "where do builds go!?!" problem.
- Thinking UMD will help. (It does if your dependencies have no dependencies – "UMD works great when you have no dependencies in your lib" – which contributes to the overall shallow dependency tree).
Ideal Solution
We all author in ES6 modules and compile to AMD for runtime. When node 0.12 ships it lets people use ES6 modules natively. The node community rapidly shifts over. By December 2015 all major browsers parse ES6 modules natively. I get a pony.
Actual Solution.
It's been 5 long years already. We're not going to convince everyone using one module system in the near future. Maybe in 2016 or 2017. Nobody likes alternate module systems. Everyone thinks their module system has "won" or will "win". Everyone is wrong.
Until that all gets resolved, I'd like to suggest we make Bower aware of Github Releases and, more specifically, release assets. Release assets:
You can also attach binary assets (such as compiled executables, minified scripts, documentation) to a release. Once published, the release details and assets are available to anyone that can view the repository.
The 10,000ft view of how this could work:
- Authors can remove built code from source control.
- Authors can author in whatever module format or language they prefer.
- Authors can publish builds as release assets that transpile from their preferred authoring format into executable JavaScript wrapped in multiple module syntaxes
- Authors can use what ever build tools they prefer to prepare these publications
- Consumers can specify preferred module formats in their projects. Bower will attempt to provide this to them.
- Consumers will get the default behavior (plain source checkout) if an asset of the preferred format is unavailable.
The lower level view of how consumers use this (which we can bikeshed):
-
In a project .bowerrc, I can specify acceptable build types in order
{
"buildAssets": ["es6", "amd"]
}
or maybe grouped by asset type?
{
"buildAssets": {
"javascript": ["amd", "global"],
"css": ["sass", "css"],
"icon": ["svg"]
}
}
-
In addition to sha/tag mappings, the bower local cache gets populated with
available build assets by name for each release when using the GitHubResolver.
-
When a consumer attempts an install Bower checks builds by name as part of the resolution phase
-
If a build is available for the resolved release version, that build is downloaded instead of the release build.
So for https://github.com/trek/multi-publish-example, asking for amd gets you
https://github.com/trek/multi-publish-example/releases/download/v0.0.1/amd.zip instead of https://github.com/trek/multi-publish-example/archive/v0.0.1.tar.gz
-
If no matching build is available, the current behavior of just obtaining the source code applies.
-
Builds can be specified explicitly and stored in the bower file so this check can be skipped later.
bower install jquery@amd
# or
bower install jquery#2.1@amd
Some problems with this proposal:
- You need to use the Github API, which is rate limited to 5000 requests/hr. With caching this may not be an actual problem in the wild.
- Projects that ship multiple asset types (e.g. Bootstrap). Do they need to make a build for every asset type combination? Convert into a metaproject that references one JavaScript, one CSS, and one Font project? Maybe they just don't participate at all?
- What is special about module formats over, say, precompiled-code formats (coffee, clojurescript, etc)? I'd argue that because they represent the standard way a library exposes its public interface, module systems have more in common with wrapping code in IIFE's than compiling from coffeescript to executable JavaScript. In my mind, if you authored in coffeescript, that code is black-boxed inside the module so your publish phase would compile from coffee to javascript, then convert your authoring module format to all compatible module formats. Other people (@domenic) disagree.
Apologies if this idea has been floated before. I did a cursory search and couldn't find anything.
The impetus behind this proposal is a long exploration of the painful JavaScript modularity story. I'll save you the read and summarize the most salient points:
These general problems also apply to other asset types (e.g. css, images), but I'll focus on JavaScript for this proposal.
Dependency Depth
@rayshan was kind enough to give me a data dump for the last 30 days of bower installs. I'm still playing around with that data, but I think I have enough to make some points. Typical dependency depth for libraries published to Bower is 0. I don't have any data on npm, but just looking into some node based projects on my computer, it's not uncommon to see dependency depths of 5 or more.
One exception to this in Bower is the clusters of libraries around foundational projects like jQuery, Ember, or Angular. Here, dependency depth is between two and three. This appears to be because these projects' communities either have a shared build ecosystem and/or a mandated modular format (ES6 for Ember, the
$namespace for jQuery,angular.modulefor Angular, etc).JavaScript outside these silos tends to lack (or not expose) modularity. This lack of modularity is leading to fragmentary solutions which complicates the process of getting dependencies into projects without a lot of additional effort on the part of the package consumer.
The Gold Standard for this should be systems like Rubygems and npm where coarse (package-level) and fine-grained (specific object import) dependency is so frictionless, you'd be crazy not to use it.
To summarize:
Gets the
foopackage as a copy locallyIs really a shorthand for the loader to go find
node_modules/foo/package.json, look for itsmainentry, and bring the contents of that file into the execution environment.In Bower the process is more like:
Problems
The lack of that common module system is forcing authors to engage in various painful-to-maintain patterns:
dist/pattern. Pain points: having amainentry in a package manifest is basically meaningless, forgetting to build before publish leads to problems where a tagged version'sdist/is still the old code, not every project uses the same folder for builds, so each consumer has to go find where code is.¯\_(ツ)_/¯" approach of shipping one module system. I chatted with @jeremyckahn about how he handles the module problem for https://github.com/jeremyckahn/rekapi/ and he basically said "what module problem?". Turns out he ships only in AMD/one global variable exposed and other format users are just not in scope. This is also true for everyone who publishes as CJS and says "just use browserify!".Ideal Solution
We all author in ES6 modules and compile to AMD for runtime. When node 0.12 ships it lets people use ES6 modules natively. The node community rapidly shifts over. By December 2015 all major browsers parse ES6 modules natively. I get a pony.
Actual Solution.
It's been 5 long years already. We're not going to convince everyone using one module system in the near future. Maybe in 2016 or 2017. Nobody likes alternate module systems. Everyone thinks their module system has "won" or will "win". Everyone is wrong.
Until that all gets resolved, I'd like to suggest we make Bower aware of Github Releases and, more specifically, release assets. Release assets:
The 10,000ft view of how this could work:
The lower level view of how consumers use this (which we can bikeshed):
In a project
.bowerrc, I can specify acceptable build types in order{ "buildAssets": ["es6", "amd"] }or maybe grouped by asset type?
{ "buildAssets": { "javascript": ["amd", "global"], "css": ["sass", "css"], "icon": ["svg"] } }In addition to sha/tag mappings, the bower local cache gets populated with
available build assets by name for each release when using the
GitHubResolver.When a consumer attempts an
installBower checks builds by name as part of the resolution phaseIf a build is available for the resolved release version, that build is downloaded instead of the release build.
So for https://github.com/trek/multi-publish-example, asking for
amdgets youhttps://github.com/trek/multi-publish-example/releases/download/v0.0.1/amd.zip instead of https://github.com/trek/multi-publish-example/archive/v0.0.1.tar.gz
If no matching build is available, the current behavior of just obtaining the source code applies.
Builds can be specified explicitly and stored in the bower file so this check can be skipped later.
bower install jquery@amd # or bower install jquery#2.1@amdSome problems with this proposal: