Maggie 3.0Meet the newly updated AI tutor

See what's new
File Extension Conventions in Magento's RequireJS Configuration

File Extension Conventions in Magento's RequireJS Configuration

Magento’s implementation of mapping JavaScript and Knockout.js template files with RequireJS may seem awkward at first glance, but there is a rationale behind the approach.

In a Magento requirejs-config.js map, leave .js off JavaScript modules but keep .html on Knockout templates. Both forms work, but when two modules override the same template, the .html mapping always wins regardless of module sequence, so the extensionless override silently loses.

Here's how I got there, and the core code that makes it happen.

How RequireJS Loads JavaScript Files

Let’s first take a look at how we would override a JavaScript file in Magento with RequireJS. The following example is a code snippet from Adobe's frontend developer docs of how this is accomplished:

var config = {
    map: {
        "*": {
            menu: "js/navigation-menu",
            "mage/backend/menu": "js/navigation-menu"
        }
    }
};

Note that the navigation menu didn’t need to be specified as js/navigation-menu.js -- the .js extension was omitted. This is because, by default, RequireJS does not require you to add the .js extension when defining paths or aliases in requirejs-config.js files.

This is not just a Magento practice, but a standard of RequireJS and is even defined in their API. The concept is also used when mapping aliases to RequireJS modules.

So far, so normal. But some confusion comes into play when Magento loads Knockout.js template files. And this is because loading KO.js templates is a Magento-specific implementation -- the ability to do this does not exist in the RequireJS core API.

How Magento Loads Knockout.js Templates

Since RequireJS has no concept of a Knockout.js template, Magento needed to create this implementation. And it does this by defining a special path to Magento module template path locations.

Here is an excerpt from my Customize the Magento 2 Checkout course, in which we start customizing the checkout sidebar. This requirejs-config.js file tells Magento to substitute out the core sidebar Knockout.js template file with our customized file:

var config = {
    map: {
        "*": {
            "Magento_Checkout/template/sidebar.html":
                "Macademy_CustomCheckout/template/sidebar.html"
        }
    }
};

This mapping loads the Knockout.js template with our custom app/code/Macademy/CustomCheckout/view/frontend/web/template/sidebar.html file. We’re essentially telling RequireJS (and Magento) that when the Magento_Checkout/template/sidebar.html is requested, load Macademy_CustomCheckout/template/sidebar.html. This is similar to overriding a class with a class preference in PHP.

Note though that the path that is defined is a completely non-standard location. The Macademy_CustomCheckout/ path defined in the requirejs-config.js file refers to either the view/frontend/web directory within our custom module, or the Macademy_CustomCheckout/web directory within a custom theme. Both of these locations are specific to Magento’s implementation.

Alternate Knockout.js Template Loading Format

In the example above, we loaded the Knockout.js template by specifying the .html extension. But that's not the only way to do it.

You can also leave the extension off entirely. The following code works just the same:

var config = {
    map: {
        "*": {
            "Magento_Checkout/template/sidebar": "Macademy_CustomCheckout/template/sidebar"
        }
    }
};

Now, since this works, it would appear at first glance that we should be using this “no file extension” approach to remain consistent with the RequireJS’s approach of loading JavaScript files. But staying “consistent” here is exactly what leads to confusing results.

Load Order With and Without .html

Let’s revisit our previous example, and assume that a third-party module (which we’ll call Vendor_ModuleA) includes the .html extension when overriding the sidebar template file:

Third-Party, Module A

app/code/Vendor/ModuleA/view/frontend/requirejs-config.js

var config = {
    map: {
        "*": {
            "Magento_Checkout/template/sidebar.html":
                "Vendor_ModuleA/template/sidebar.html"
        }
    }
};

Now let’s assume that you are creating a customization for a client and would like to override the same file. You create an override in your own module (Macademy_CustomCheckout, which we’ll call Module B), but this time, without using the .html extension:

Local Code, Module B

app/code/Macademy/CustomCheckout/view/frontend/requirejs-config.js

var config = {
    map: {
        "*": {
            "Magento_Checkout/template/sidebar":
                "Macademy_CustomCheckout/template/sidebar"
        }
    }
};

What will happen in this situation? The .html mapping wins, every time, no matter how you sequence the modules. That's predictable, just not in the way you'd expect. Module A's template renders, and your Module B override is silently ignored.

Not only will this cause you massive confusion with trying to figure out why your updates aren’t applying, but you can't fix it with load order. If you instead use the .html extension in your custom override, both modules declare the exact same map key. Magento merges every module's requirejs-config.js in module load order, and the later one wins for a duplicate key, so adding <sequence><module name="Vendor_ModuleA"/></sequence> to your module.xml puts your override in control. That's a much more predictable outcome for your code.

Why the .html Mapping Always Wins

This isn't a coincidence, and you can see why in core. When a UI component asks for a template like Magento_Checkout/sidebar, Magento's template loader at Magento_Ui/js/lib/knockout/template/loader builds the RequireJS module ID with formatPath():

formatPath: function (path) {
    var result = path;
 
    if (!hasPlugin(path)) {
        result = defaultPlugin + '!' + result;
    }
 
    if (isFullPath(path)) {
        return result;
    }
 
    if (!hasFileExtension(path)) {
        result += '.' + defaultExt;
    }
 
    return result.replace(/^([^\/]+)/g, '$1/template');
}

defaultPlugin is text and defaultExt is html, so Magento_Checkout/sidebar becomes text!Magento_Checkout/template/sidebar.html. RequireJS applies map to that resource name first, and only the key with .html matches it exactly (map keys match on whole path segments, and sidebar.html is not sidebar).

The extensionless key only gets a chance later. The text plugin (lib/web/mage/requirejs/text.js) resolves the file with req.toUrl(), which strips the extension, applies map a second time, then puts .html back on. By then, an .html mapping has already swapped the name, so the extensionless key never matches anything.

Why Use .html for Knockout.js Templates?

Just because we can omit the file extension when loading Knockout.js template files, doesn’t mean we should. And there are a few solid reasons for this:

  • Consistency: Magento has established conventions for how to load files, and sticking to these conventions helps to maintain consistency across your projects. When you start deviating from standards, you only create additional confusion for other developers (and your future self) to understand what’s going on in your code. This is the same reason that we omit the .js extension for JavaScript files: it’s because this is the expected approach when working with JavaScript files in RequireJS, and defining the .js extension actually makes your code less consistent.
  • Compatibility: By following these conventions, you are also ensuring your custom themes or modules are compatible with the larger Magento ecosystem. This reduces the possibility of introducing conflicts or bugs in your code, which in turn, leads to code that meshes better with third-party code.
  • Predictability: When the .html extension isn’t defined, it’s very possible that a JavaScript file located in the same place can be loaded instead. I’m not saying this will ever happen; but it’s possible, and you want your code to be as predictable as possible, especially when there are no advantages to omitting the extension.
  • Self-Documentation: When another developer looks at your code, you want it to be very obvious whether you are dealing with a JavaScript file or a Knockout.js template. By leaving the .html extension off, you are creating an ambiguous situation where it’s possible the developer will get confused and not be able to tell if you are overriding a JavaScript file or a Knockout.js template, or some other file.

My Takeaway

So, should you always include the .html for Knockout templates, and omit the .js for JavaScript files in your RequireJS configs?

Yes. Include .html for Knockout templates, and leave .js off JavaScript.

Magento has conventions defined for a reason, and when you start deviating from the expected norm, you start creating unexpected results. You want your app’s code to be routine, compatible, predictable, and consistent with the broader ecosystem.

Is there ever a situation or scenario in which omitting the .html extension is useful? Not likely.

While there could always be very niche cases where this may apply, deviating from the norm should be looked at as a temporary workaround -- and the term “temporary workaround” almost always sounds like a hack, rather than a practice that you should rely on.

Template overrides are only one piece of the checkout puzzle. If you're working in the same area, see how local and external JavaScript state works in Magento and how to change checkout fields with custom layout processors and jsLayout. And if Knockout itself is the part that keeps tripping you up, the Knockout.js course walks through it from the ground up.

If you've ever lost an afternoon to an override that wouldn't apply, I'd like to hear which map key turned out to be the culprit.

Something here out of date?

Magento moves, and so does this site. If a step no longer works, tell me and I'll fix it.

Get the next one by email

Every other week I send a short newsletter on how PHP + Magento actually work: releases, security alerts, and one thing explained under the hood. Enroll in the Library and it's yours. No credit card needed.

The University includes every PHP + Magento course available, and every new M.academy course as it ships.