Skip to content

Amber Config

Hannes Hirzel edited this page Apr 17, 2015 · 22 revisions

amber config

In version 0.12 an amber config command was introduced to generate the config.js and other ..... files.

Since version 0.13 there are the grunt devel and the grunt deploy commands. They call amber config.

The notes below explain what the amber config command (or thus grunt devel) does.

Whys, whats and hows of Amber modularization and loading

Aim of configuration in Amber

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

Why is AMD / RequireJS used?

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 version 0.12 the Amber code base was more monolithic. Amber needed to be in the driver seat and it was hard to write code in separate modules and use libraries located in different stores. You had to have all the Amber code in one place, similarly to the image files of classical Smalltalk implementations.

The main reason for changing the loader was to allow to load, and possible save, Amber packages from different locations. The goal is to allow create self-contained Amber libraries that are easy to compose. This change process started in version 0.11 by adapting the custom loader to multiple locations.

How is the configuration done in Amber ?

When running grunt devel or grunt deploy a summary AMD configuration for your projects is generated. This configuration contains

  • the path mappings,
  • shim dependencies, and
  • other pieces of configuration;

all of them collected for all parts of your project.

The configuration is saved in a config.js file. This is more or less just one big require.config call with all that configuration data 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.

In addition since produced config.js contains all the needed mappings in one place, you can use it as mainConfigFile parameter for the 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.

I do not run amber config but still have config.js generated. How come?

The commandline amber config still does the job. But since version 0.13, if you created the project with amber init, it contains pregenerated Gruntfile.js with definition of some development tasks. Some of those tasks also generate the config.js, among them grunt devel (set project in development state, regenerating what is needed to get there) and grunt deploy (put project in deployment state, regenerating what is needed to get there). The former is automatically run by amber init, so the new project starts in development state with freshly generated config.js.

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 issue grunt devel from CLI. It recreates your configuration. You just have to 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 grunt devel. 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 an Amber library is highly likely grunt devel compliant and have *.amd.json manifest file(s). grunt devel uses these files to build config.js. If you add a JS library which does not have a manifest file, you will need to add it (more in "But grunt devel does not work for JS libraries I use in my app" part of this doc). This is often a 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 does Amber need them ?

To have namespaces is consequence of:

  1. Amber packages being AMD modules;
  2. The way path mappings work in AMD configuration.

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 an 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 file. Amber does not have an image. Web applications are most often composed of pieces coming from different URL locations.

In AMD, modules have symbolic names like jquery, amber/devel or helios/Helios-Core. These symbolic names look like URL paths - they indeed work as such, and / is the path separator. You can config your 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 the role of configuration in Amber 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 configuration are placed in JSON notation. Amber itself has one, Helios has one, your new app created by amber init (Amber version 0.13) has it.

The function of the configuration process in Amber

The grunt devel command takes local.amd.json files, obtains all the mappings and other config infos from them, and merges these configuration parts into one big require.config call written into the config.js file. 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.

Do I need to put the dependency libraries into the bower_components directory?

No. As mentioned above you can put the library wherever you want inside your application directory tree. The key requirement 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 dependencies for you automatically.

What is bower ?

Look at bower.io for more. In essence, it is the repository of client side web packages. Anyone can register a package by pointing to the place where the versions of the package can be found. Each package also lists all its dependencies along with their version constraints - so whenever you install a bower package, all its dependencies are searched for and installed as well in the correct versions. By default, bower puts all installed packages into a directory called 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 grunt devel 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 the appropriate file.

Until that time where all JS libraries will include their local.amd.json, you should provide manifest files for them. Not by putting local.amd.json into their directory - this would be destroyed by any 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 check out how Amber and Helios use external JS libraries. 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 grunt devel tries to solve in big picture - assembling configuration from its local pieces. Putting info on JS library dependency into local.amd.json itself is an easier way, but makes thing more entangled in the long run. For example, if you remove the library, you should find its setting all over local.amd.json - in paths, in shim, etc. With libdir.amd.json, you just remove the file and run grunt devel.

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

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

Clone this wiki locally