Guide 03 · Developer docs

The theme repo and the asset pipeline

Everything a developer changes on a Core dna site lives in its theme repository. This guide covers the repository and its build, printing records in templates, stylesheets and client-side code, and the parts of a site around the main content.

L1–L2
Unit 04 · Client-side code

Entry files and modules

An entry file should only import modules and start them; a module should do nothing until it is started. The init convention from a production TypeScript repository, why it keeps a shared module safe in several bundles, and what an init function must do when its element is not on the page.

Who this is for. The developer adding a feature to a theme's JavaScript. Afterwards you can add a module that loads in any bundle without side effects, start it from an entry in one line, and keep one broken feature from taking the others down with it.

Entry files are manifests

An entry is the file a bundle starts from: main.ts or index.js, one per bundle. It should read as a list of what the bundle does, and nothing else. The cleanest production example, in a Twig repository written in TypeScript, imports one init function per feature and runs each in turn:

JavaScript src/js/rible/main.ts (shortened)
import { initModals } from '../components/modals';
import { initVideo } from '../components/video';
import { initTracking } from '../tracking';
import { initCaptchas } from '../helpers/captchas';
import { initFreeTrials } from '../forms/free-trial';

const initializers = [initModals, initVideo, initTracking, initCaptchas, initFreeTrials];

const initialize = () => {
  initializers.forEach((initializer) => {
    try {
      initializer();
    } catch (e) {
      console.log(e);
    }
  });
};

initialize();

The try around each call is deliberate: a feature that throws, because the markup it expects has changed or a third-party script has not loaded, is logged and skipped, and the rest of the page keeps working. Without it, the first error stops every initialiser after it.

Modules do nothing when imported

A module exports functions and does no work at the top level: no listeners attached, no DOM queried, no globals assigned, no requests sent. The work happens only when its init function is called.

JavaScript src/js/components/modals.ts
export function initModals(root = document) {
  const triggers = root.querySelectorAll('[data-modal-open]');
  if (!triggers.length) {
    return; // not on this page: do nothing, quietly
  }

  triggers.forEach((trigger) => {
    trigger.addEventListener('click', () => openModal(trigger.dataset.modalOpen));
  });
}

function openModal(id) { /* … */ }

Three things follow:

  • A shared module is safe in several bundles. Two entries that both import modals on a page that loads both bundles do not attach two sets of listeners at import time; each init runs only where an entry calls it.
  • An unused module costs only its bytes. Imported and never started, it does nothing at runtime.
  • An init function returns quietly when its element is absent. Entries run on every page of their type, and most pages do not have most features.

Accepting a root element, as above, lets the same function start the feature inside markup added later, such as a fragment loaded with JavaScript.

When something must be global

Some platform scripts call functions by name, for example a form's result callback, named in its data-submit-result-callback attribute. Assign those to window explicitly, in the entry, in one place: Object.assign(window, formCallbacks). A global assigned from inside a module at import time is invisible in the entry and breaks the rule above.

Next

Loading jQuery without a second copy is jQuery, loaded once; typing the data templates hand to scripts is TypeScript in a theme.

Go deeper

Was this page helpful?

Your answer goes to the docs team.