Skip to content

Amber Config

Herbert Vojčík edited this page Aug 1, 2014 · 22 revisions

amber config

Whys, whats and hows of Amber modularization and loading

Why amber config ?

To easily assemble AMD mapping for a loader, such as RequireJS.

Why AMD / RequireJS ?

To use proven, tested loader / modularization technology which would allow:

  • Amber to use Amber packages from multiple sources and to some extent even develop them in parallel (save to multiple locations).
  • Amber to cease being in the driver seat and be embeddable to different settings.
  • Amber to use external non-Amber libraries from multiple sources.

Before introducing RequireJS in 0.12, Amber has monolithic nature - it had to be in the driver seat and it was hard to write code in separate modules and libraries located in different stores - you had to have all the Amber code in one place, similarly to images of classical Smalltalks.

The main 'why' for changing the loader (which began on 0.11 already by adapting the custom loader to multiple locations) was to allow to load, and possible save, Amber packages from different locations. This would allow creating Amber libraries of its own.

So what does amber config do ?

It creates a summary AMD configuration for your projects, where all the path mappings, shim dependencies and other pieces of configuration are collected for all parts of your project; and saves them in a config.js file, which more or less consist only of one big require.config call with all that configuration passed in.

As a result, if you build an Amber app, you only need to load config.js to have all namespaces of your app, libraries and Amber itself mapped, shim dependencies set, etc. The only thing you need to do after loading config.js is to have RequireJS (or other AMD loader) loaded and issue a require call to load packages of your app.

On the plus side, since produced config.js contains all the needed mappings at one place, you can use it as mainConfigFile parameter in r.js optimizer - which takes all .js files, .css files and possible also other parts of your app and minimizes and bundles them to one single .js file for deployment.

What's so good about it ?

Whenever you add, update or remove dependencies in your application (including Amber), or you change some piece of local configuration (move your files around), you basically do not need to do much more than just issuing amber config from CLI. It recreates your configuration so it is actual, so you can just reload your page.

This scenario can really work: you find some Amber library you want to use. It is published on bower. You do bower install library-i-needed --save, then you run amber config. All namespaces that are present in the library as well as all other symbolic names it uses for its dependencies are immediately present in your config.js. You may just start to use the library, by adding it to your require call in index.html to load it and you can start writing code that uses it.

This is because Amber library is highly likely amber config compliant and have *.amd.json manifest files. amber config uses them to build config.js. In case of adding JS library which does not have manifest files in it, you will need to add them (externally, not writing them in the directory of the library) yourself, which is often one-time task and does not take more than a few minutes (unless you get stuck on petty details like "what name should I use for this library").

I don't like namespaces. Why Amber needs them ?

Having namespace is consequence of:

  1. Amber packages being AMD modules now, and of
  2. the way how path mapping works in AMD.

AMD is about defining modules in form of define ([module_name], array_of_dependency_names, factory_fn);. That is, a file with AMD module calls define to register a module, passing list of names for all dependencies, and a function which will instantiate the module once it's needed. No code of the module is actually run until the module is required, either directly, or indirectly as a dependency of some required module. In this way, modules can be loaded asynchronously, and AMD loader can create the modules from their factory methods in dependency order (require call is itself also asynchronous: require(array_of_required_module_names, callback_fn)).

In Smalltalk, packages reference parts of each other a lot. Subclasses are often created from classes of different packages. Extension methods are provided for classes of different packages. As such, a tree of loading dependencies is present in any Amber app.

Since AMD module must be able to list its dependencies, statically, in a define call, each package must have its canonical name for AMD. This is the first of the reasons listed above. So far, just using the name of the package itself, maybe with some hard-coded prefix, would be enough. But there's more.

If you build your application from more package groups (Amber core, libraries, your app code itself), you have source code (and its compiled counterpart) from different sources. You do the same thing in Pharo, but there Monticallo takes care of loading them for you and they are then all mixed in to the image. Amber does not have image; and web app are often composed from different pieces from different locations.

In AMD, your module have symbolic names like jquery, amber/devel or helios/Helios-Core. These symbolic name looks like unix paths - they indeed work as paths, and / is the path separator. You can config you AMD loader with path mappings - you can map any path prefix into url path. So you may instruct AMD to look for jquery in bower_components/jquery.min (.js should be omitted). If jquery itself is required, it is assumed to be a file, and bower_components/jquery.min.js is loaded. But you may also instruct the loader to look for helios in bower_components/helios. So if helios/Helios-Core is required, loader looks for it in bower_components/helios/Helios-Core.js. In other words, you can map any portion of the path - if used by itself, it is used as a file name (with .js added automatically), if used as path prefix, it is used accordingly.

This is the second reason listed above. To allow for easy modularization of Amber libraries, each of it can reside in a place of its own - you can map its path prefix to local path or even to URL, and it will work.

For this to work, each Amber library (a set of packages developed and deployed as a group and thus residing in the same location) must have its definitive path prefix that can be mapped. The prefix must be definitive because of the first reason.

The namespace is this prefix.

So what is role of amber config in this again ?

Each library should have a manifest file called local.amd.json in its root directory, where all the path mappings and other pieces of config needed are placed, in JSON notation. Amber itself has one, Helios has one, your new app create by amber init for 0.13 version of Amber has it.

amber config, shortly speaking, just takes those local.amd.json files, takes all the mappings and other config infos, and merges them in one big require.config call written into config.js. All path mappings in local.amd.json are interpreted relatively to the directory of the library itself, so no matter where you actually put the library in your app directory layout - if it is there, it's local.amd.json is found, and incorporated into config.js, with paths properly resolved.

Most often, the library's local.amd.json is as simple as

{
  "paths": {
    "library-namespace": "src"
  }
}

(btw, this is what default local.amd.json that amber init creates for you by default looks like)

That is, it map library-namespace to local src directory; this is picked by amber config so library-namespace is mapped to path/to/library/src in config.js.

Must I use bower_components to put libraries in ?

Absolutely not. As already said in previous answer, you can put the library whenever you want inside you app directory tree. The key is that amber config can find its local.amd.json. If you want to use any other layout (and ways to get your library code), you can, though bower is very convenient and solves depndencies for you automatically.

What is bower ?

Look at bower.io for more. In essence, it is the repository of web packages. Anyone can register a package by pointing a place where the versions of the package can be found. Each package also list all its dependencies along with their version constraints - so whenever you install bower package, all its dependencies are looked for and installed as well in correct versions. By default, bower puts all installed packages into directory bower_components.

Amber itself and Helios IDE are both bower packages. Any new application / library you create using amber init has its bower manifest created already, so you can register it as a bower package as well.

But amber config does not work for JS libraries I use in my app

Well, I have a dream: jQuery will include local.amd.json in its distribution which will map 'jquery' to appropriate file.

Until the time where all JS libraries will include their local.amd.json, you should provide manifest file for them. Not by putting local.amd.json into their directory - this would be destroyed by and update or similar operation. On the contrary, you put dirname.amd.json out of the library directory - ideally besides your own local.amd.json. If the library is installed in some/path/dirname, then dirname.amd.json will be used just as if it was local.amd.json inside that directory. This way, you defined .amd.json externally for any directory holding the external JS library, where you can put the path mappings and often also shim the dependencies (see RequireJS docs for this or other advanced pieces of AMD configuration).

You can see for yourself that Amber and Helios use external JS libraries themselves, and they also define their external .amd.json manifests. So should you if you use such library in your app.

Why not put JS library config into local.amd.json itself ?

You can, but this introduces unnecessary coupling. In essence, this is just smaller instance of the same problem amber config tries to solve in big picture - assembling configuration from its local pieces. Putting info on JS library dependency on local.amd.json itself is easier way, but makes thing more entangled. For example, if you remove the library, you should find it setting all over local.amd.json - in paths, in shim, etc. With libdir.amd.json, you just remove the file and run amber config.

So local.amd.json should only contain the initial mapping of my app namespace ?

Until you create one more namespace in your app, yes. You can of course have more namespaces in your app. Each can be mapped to different directory, but they can also be mapped into same one and you use the different namespace just for separation of concerns (it later allows you to painlessly move files with same namespace to different dir or even to different library).

Clone this wiki locally