X

Technical Articles relating to Oracle Development Tools and Frameworks

  • January 3, 2020

Advanced Components - JET Packs Part 1

Duncan Mills
Architect

Last Updated Jan 2020 for JET 8

First Some Background

If you are a close observer of the JS documentation for Oracle JET you may have noticed references to things called JET Packs in relation to Custom component metadata.  In this article I wanted to cover the background to JET packs, as an essential foundation to a series of more advanced topics relating to custom components.  But before we go there, let's step back and consider a couple of inherent problems that can arise with Custom Web Components once you start to get serious in their use. 

Referencing

First of all, let's consider the problem of how you load a component in the first place.  If you use the ojet command line tool and create a component within your project called something like ccdemo-name-badge, you can reference the loader script simply using 'ccdemo-name-badge/loader' in your define() statement (or import if using TypeScript). This works because the JET Command line tool has adopted a pattern called reliable referencing as a way to reference any component. This means that it will automatically create a requireJS path mapping with the same name as the component in the built-application.  You can see this for yourself if you look at the main.js that is generated in the /web folder by the ojet build command, you would see an entry reading something like this:

requirejs.config(
  {
    baseUrl: 'js',
    // Path mappings for the logical module names
    paths:
    // injector:mainReleasePaths
      {
        "knockout":"libs/knockout/knockout-3.5.0.debug",
        ...,
        "ccdemo-name-badge":"jet-composites/ccdemo-name-badge/1.0.0"
      }
  }

What happens here is that the ojet tool scans your src/js/jet-composites (and /src/ts/jet-composites) folders for any components and injects the correct paths for any it finds into the built application using the name of the component as the name of the path.  This makes it trivial to switch the application to use a different version of the component perhaps even reading it off a CDN somewhere rather than it being embedded in the application. Plus of course it's highly predictable (and this is the core principle of reliable referencing) - there is always a requireJS path with the same name as the component to reference it.  This then allows a different component to reference this component safely using this convention. 

So reliable referencing is a key principle, however, it does have a problem; simply that of verboseness if you are using a lot of components.  In that case you will have a separate path mapping for each component used which might bloat your path mappings considerably. 

Version Management

The referencing problem is only a minor one and is more of an offence to our tidy programming sensibilities rather than being a significant issue in terms of performance, for example.  The second problem with use of multiple components is much more significant, that of dependency and version  management.  Although many components can truly be regarded as standalone (we call these "singleton" components) others might start to have dependencies.  Now these dependencies might be to other components, or to other shared resources or libraries, all of which can start to introduce a challenge. For example let's assume that I manage three components each with several versions and those components are used across three applications e.g.

You can see from even this simple case there are a large number of possible combinations of component versions that might be assembled into any given application.  Should you be testing every single possible combination? (40 of them in this simple case) The answer is probably not. Furthermore, how will you know if upgrading one of those individual components is safe?  What it it has a dependency on a particular library version  that is incompatible with one of the other components that your are using? 

So this is what JET Packs help to simplify, although it is the combination of both JET Packs and the Component Exchange which is really needed to completely address the problem space.

Understanding JET Packs

Given the problems outlined above, we have introduced the JET Pack component type as a tool/artefact to help. What a JET Pack does (in simple terms) is to define a stripe of component versions that are certified together. This is aimed at teams which are creating "sets" of components that need to be released as a certified bundle.  Downstream consumers have a dependency on a given version of the JET Pack and this effectively gives them a known set of versions for all of the components that are part of that version of the JET Pack. In the next article I'll discuss what this means in terms of disk layout, but for now I'll just concentrate on the actual definition of a JET Pack and the member components within it. 

Using a similar picture to the one above, using packs we can simplify how applications consume components, rather than referencing specific versions of each component that they use, they reference a particular version of the JET Pack, which by definition defines the stripe of pack component versions that they will be using:

This makes the management of both referencing and upgrades much easier.

To cover upgrades first of all, it's evident that from one version of the JET Pack the only possible upgrade is to a newer version of that same pack.  Implicitly we know that the components within that target JET Pack version are an "approved (and tested!) stripe of components which will all work togther.

When using a JET Pack we simplify the whole referencing problem as well.  Rather than each component having its own requireJS path to reach the loader, instead the JET Pack as a whole has a single defined requireJS path (again this uses the same name as the component). All components are then loaded relative to that path.  So now, that single requireJS path mapping can be used to reliably reference a whole set of components.  What's more, to "upgrade" the JET Pack or to switch between minified and debug versions, you just have to change that one path entry and everything just works. This vastly simplifies the ongoing management of component use within your applications. 

Defining a JET Pack 

In physical terms, a JET Pack is a component in its own right with a type attribute of "pack". Just like other components, it uses a component.json file to describe itself, but unlike normal UI (type="composite") components  it needs no further files (excepting maybe a README.md file and a LICENSE.txt) .  The component.json and these supporting files are then just zipped up to create the distributable pack component. It's also worth noting that a JET Pack component has no part to play at runtime, with the exception of the disk layout it enforces it's purely a design time artefact used by tooling to manage the installation and upgrade problem space. 

Here's a typical component.json file from a JET Pack, this is the sample component set that Oracle ships called oj-sample.

{
  "name": "oj-sample",
  "displayName": "oj-sample JET Pack",
  "type": "pack",
  "version": "2.3.0",
  "description": "Consolidating JET Pack for a set of example components for use in JET and Visual Builder",
  "license": "https://opensource.org/licenses/UPL",
  "dependencies": {
    "oj-sample-visualization-exporter": "2.0.1",
    "oj-sample-qr-code": "2.0.1",
    "oj-sample-timed-event": "2.0.1",
    "oj-sample-flip-card": "2.0.1",
    "oj-sample-drawer": "2.0.1",
    "oj-sample-country-picker": "2.1.0",
    "oj-sample-utils": "2.0.2",
    "oj-sample-markdown-viewer": "2.0.1",
    "oj-sample-copy-text": "2.0.1",
    "oj-sample-show-when-ready": "2.0.1",
    "oj-sample-calendar": "2.0.1",
    "oj-sample-calendar-provider": "2.0.1",
    "oj-sample-calendar-event": "2.0.1",
    "oj-sample-organization-tree": "2.0.2",
    "oj-sample-organization-tree-item": "2.0.2",
    "oj-sample-tooltip": "1.0.2",
    "oj-sample-highlight-text": "1.0.1",
    "oj-sample-input-text-typeahead": "1.0.0"
  }
}

So, to break this down there are some key attributes in the metadata that we need to discuss.

  • name - The name of the JET Pack that will be used to define the requireJS path that leads to all of the components in the pack.  Note how this matches the start of all of the component names - in this case they all begin with oj-sample-*. This is required, we can't combine components from multiple namespaces within the same JET Pack. 
  • type - is pack, no surprises there. 
  • version - is the version of the pack as a whole.  It is this version that consuming applications will care about.
  • dependencies - This describes exactly what components/versions make up this version of the JET Pack.  Notice how it's fine to have a mix of version numbers here, there is no expectation that all of the components in a JET Pack stripe all have the same version number. The other important thing notice here is that the version numbers used are absolute, not semver ranges as you might use in a UI component (composites) dependency metadata.

Notice that there is no mention of the jetVersion attribute in the JET Pack metadata.  This will be inherited from the components within the JET Pack version stripe.

Defining a Component in a JET Pack

We've seen how a JET Pack as an artefact is really just a index card listing a bunch of related component versions that are certified together, but what about the components themselves?  These still need to be defined in exactly the same way as singleton components are (and would be zipped up into individual ZIP files in the same way).  There are, however, a few small issues to address:

  1. The loader.js script 
  2. Differences in the component metadata
  3. How to reference (load) a components in a JET Pack 

Let's look at each of those.  To do so, I'll use oj-sample-calendar component as it has a bunch of dependencies that are typical for a moderately complex scenario. 

The Loader Script 

Recall that the responsibility of the loader.js script is to ensure that the tag name for the custom component is registered with the browser. When we are defining a component within the context of  JET Pack this is no different. e.g. the calendar loader script looks like this. 

define(['ojs/ojcomposite', 'text!./calendar-view.html', './calendar-viewModel', 'text!./component.json','css!./calendar-styles.css'],
  function(Composite, view, viewModel, metadata) {
    Composite.register('oj-sample-calendar', {
      view: view, 
      viewModel: viewModel, 
      metadata: JSON.parse(metadata)
    });
  }
);

My point here is that there is no difference at all between the loader script for a singleton component or a pack component - in both cases the name passed to Composite.register() has to be the name of the HTML tag being registered.  The browser neither knows or cares if the component is a singleton or a member of a pack.

The Metadata

The main difference between a singleton component and a component in a JET Pack can be found in the metadata and it's only a couple of small changes. Here's an extract from the metadata for oj-sample-calendar again.

{
  "name": "calendar",
  "pack": "oj-sample",
  "displayName": "Calendar",
  "description": "Fully featured calendar component",
  "license": "https://opensource.org/licenses/UPL",
  "version": "2.0.1",
  "jetVersion": ">=6.1.0 <8.0.0",
  "dependencies": {
    "oj-ref-moment": "^2.22.2",
    "oj-ref-fullcalendar": "^3.9.0",
    "oj-sample-calendar-event": "^2.0.0",
    "oj-sample-calendar-provider": "^2.0.0"
  },
...
}

Three attributes to take notice of here:

  • pack - Not surprisingly this tells us that the component is a member of a JET Pack and the relevant pack is named.  This is a back-reference if you will that tells tooling such as Visual Builder that if this component is asked for you actually need to install it in the context of the pack.  It's a bit like getting married, you don't just have a relationship with your spouse but you also get their whole family at the same time, you have no choice about who your mother-in-law is, that's fixed by the husband or wife you have joined with. Simply put, once you install one component from a particular JET Pack version then if you install another component from the same JET Pack then the version of that component that you get will be pre-determined by the JET Pack version set by the first, you can't pick some other version (which would be "uncertified"). In this example the pack attribute is oj-sample of course.
  • name - you'll notice here that the name of the component is simply "calendar". This differs from the singleton case where the name of the component is (should be!) the same as the registered HTML tag. So when dealing with JET Pack based components the registered HTML tag name is a concatenation of [pack + hyphen + name] e.g. "oj-sample" + "-" +"calendar" === "oj-sample-calendar".  We also call this the "full-name" of the component and that's the value that will be used when installing the component or defining dependencies on it..
  • dependencies -  in this case, oj-sample-calendar has a mix of dependencies including some to external libraries which I'll be covering in a subsequent article. However, of note are the dependencies for the oj-sample-calendar-provider and oj-sample-calendar-event components in this case. These are components in the same JET Pack as oj-sample-calendar, however, we still define the dependency in terms of the full-name of the component (including the pack prefix) and the version mentioned in the dependency can still be a semver range as shown here, although in reality the JET Pack metadata will "choose" the actual version that is co-installed when the dependencies are resolved during installation. This is a point to be careful of when defining JET Pack based components.  If you do define a semver range for your dependencies (something which is often desirable) then you have to make sure that the range selected for a component within a particular JET Pack version will overlap with the members of that stripe.

Referencing a JET Pack Component

As mentioned, we use the reliable referencing pattern to access the loader script of a given component.  When dealing with singletons this is based off of a requireJS path that matches the name of the component.  With JET Pack based components, however, it uses a path based off of the name of the JET Pack. Thus to load the oj-sample-calendar component we use: oj-sample/calendar/loader Note how the "path" to the loader is based on the generated requireJS path for the JET Pack (oj-sample) followed by a sub-directory for the calendar component which uses the component name attribute  as its name. (again structure on disk will be covered in the next article). 

Taking this one step further let's look at the viewModel for oj-sample-calendar where the define block loads the required sub-components:

define(
  ['ojs/ojcomposite', 
   'ojs/ojcontext', 
   'knockout', 
   'jquery', 
   'ojL10n!./resources/nls/calendar-strings',
   'moment', 
   'fullcalendar/fullcalendar.min',
   'ojs/ojbutton', 
   'oj-sample/calendar-provider/loader', 
   'oj-sample/calendar-event/loader'],
   function (Composite, JetContext, ko, $, componentStrings, moment) {
     function CalendarComponentModel(context) {
 ...

So again the same reliable referencing pattern, based on the JET Pack path is used to locate the two loader scripts.

Q&A

In the next few articles I'll be diving into some of the more tangible aspects of JET packs such as how they work with Visual Builder, Component Exchange and the ojet command line tool, but to wrap up this article I thought I'd end with some common questions that I get asked in relation of JET Packs - hopefully this will cover any outstanding questions you might have as well although feel free to ask any additional questions in the blog comments. 

Q. Do I have to use JET Packs? 

A. No, not at all. If you only have a simple set of components with minimal dependencies then using a JET Pack may not offer you any real value.  However, once you start to release larger sets of components with downstream dependencies you will find quickly that it makes a lot of sense to do so.

Q. Can I start out with singleton components and move them to a JET Pack later?  

A. Yes, providing that the namespace of the JET Pack encompasses the components that you want to add to the pack then this is a trivial exercise for you the producer.  It will, however, be a bit more work for any consumers that start out with the singleton versions and want to upgrade to the JET Pack version. Therefore, the advice is to seriously consider using a JET Pack from the get-go for any components you know will have a extended lifespan across multiple applications.

Note that a given version of a component cannot be both a singleton and a member of a JET Pack at the same time. 

Q. Does a JET Pack component ZIP contain all of the components in the pack? 

A. No, the JET Pack is really just a simple metadata file (a component.json) that provides the definition of a given strip of component versions.  Each version of the JET pack will define a different stripe. The components are deployed as separate ZIP files in the Component Exchange. This allows the same version of a given component within the JET pack to be used across multiple versions of the JET Pack without being duplicated in each instance. 

Q. Can a component be used across multiple JET Packs?

A. No, that would make dependency resolution impossible.  A component can only be part of one pack

Q. Can the same version of a component be used in multiple versions of a JET Pack?

A. Yes, some components will be very stable and that same version can be used for multiple versions of the JET Pack that it is a member of.

Q. How are JET Packs versioned?

A. JET Packs should use the same semantic versioning scheme as singleton components would.  When releasing a new version of a JET Pack the version number change should reflect a summary of the changes to the stripe of components referenced by the pack.  For example, if a new version of a JET Pack contains changes to two components, one being a patch change and the other being a major version number change then the JET Pack major version number segment should also be changed to reflect the summary of the impact.  Likewise if you remove a component from a pack that would be a major version change (it could break a consumer who depends on that deleted component). If you add a new component in a JET Pack release that can be regarded as additive and non-breaking so that would only need a minor version number segment change

Q. How do I install a JET Pack?

A. Strictly speaking the answer is that you don't.  You install the components that you need.  The fact that the component is part of a pack will effect where that component gets installed to / referenced from but that will be covered more in the next article in this series

JET Custom Component Series

If you've just arrived at Custom JET Components and would like to learn more, then you can access the whole series of articles on the topic from the Custom JET Component Architecture Learning Path

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.