Using ESM in Homey Apps

Since Homey v12.0.1 it's possible to create Homey apps using ECMAScript Modules (ESM). You can refer to the Node.js ESM documentation for more details. ESM brings certain advantages such as improved module isolation and native support for asynchronous loading, making it a more robust choice for modern JavaScript applications.

CommonJS vs. ESM

Before this release, Homey apps used CommonJS (CJS) for module management, requiring modules with require() and exporting them using module.exports. However, with ESM, modules are imported and exported using import and export statements.

CJS Example:

'use strict';

const Homey = require('homey');

class MyApp extends Homey.App {
  async onInit() {
    this.log('MyApp has been initialized');
  }
}

module.exports = MyApp;

ESM Example:

import Homey from 'homey';

class MyApp extends Homey.App {
  async onInit() {
    this.log('MyApp has been initialized');
  }
}

export default MyApp;

Using .mjs vs. esm: true

There are two ways to opt into using ESM in your Homey app:

  1. Using the .mjs extension: Rename all your JavaScript files with the .mjs extension. This tells Node.js that these files should be treated as ESM modules. You can also go file by file, converting part of your app to ESM without needing to rewrite everything at once. For files still using CommonJS, they can continue using require() and module.exports while coexisting with the ESM-based parts.

  2. Setting "esm": true in app.json: You can keep using the .js extension, but you need to specify "esm": true in your app.json file. This treats all .js files as ESM modules automatically.

If you go this route, make sure you change all your require() statements to import, and your module.exports to export.

Mixing ESM and CJS: Be cautious when mixing ESM and CommonJS. ESM supports asynchronous loading, while CJS is synchronous, so certain patterns may behave differently.

Example of loading a CommonJS module in ESM:

// Load CommonJS module dynamically
const cjsModule = await import('cjs-module');

Migrating from CommonJS to ESM

If you're updating an existing Homey app that uses CommonJS, here are some steps to migrate it to ESM:

  1. Replace require() with import:

const Homey = require('homey');  // Before
import Homey from 'homey';       // After
  1. Replace module.exports with export:

module.exports = MyApp;  // Before
export default MyApp;    // After
  1. If you want to keep using .js file extensions, ensure that you add "esm": true to your app.json.

  2. Check for compatibility: Make sure any third-party modules you are using also support ESM.

  3. Upgrade your app's compatibility to v12.0.1 in app.json.

Common Gotchas and Compatibility

When switching to ESM, there are some important considerations:

  1. No require() in ESM: If you need to load a CommonJS module from an ESM module, you'll have to use import() as require() is not available in ESM.

  2. No __dirname or __filename in ESM: In ESM, __dirname and __filename are not available. You can use the following workaround:

import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  1. Mixing ESM and CJS: Be cautious when mixing ESM and CommonJS. ESM supports asynchronous loading, while CJS is synchronous, so certain patterns may behave differently.

Benefits of Using ESM

Here are some reasons why using ESM in your Homey apps can be advantageous:

  • Asynchronous Module Loading: ESM supports asynchronous loading, which can lead to performance improvements, especially in environments where modules are loaded on-demand.

  • Compatibility with Modern Syntax: ESM is the standard for JavaScript moving forward, and its syntax is more aligned with other JavaScript features such as import and export.

  • Improved Developer Experience: ESM simplifies code structure with a cleaner and more intuitive module syntax. Features like named exports make it easier to track what's being imported/exported.

  • Better Support for Static Analysis: ESM allows tools and IDEs to analyze the structure of your code more effectively, leading to better autocomplete, refactoring, and error-checking features.

  • Native Support in Browsers: ESM is natively supported in modern browsers without the need for bundlers like Webpack. This can simplify app development for projects that need to run in both Node.js and browser environments.

  • Strict Mode by Default: All ESM modules run in strict mode by default, enforcing a stricter syntax and catching more common mistakes at runtime.

  • Top-Level await: ESM allows the use of await at the top level, which simplifies asynchronous code in modules without needing to wrap everything in async functions.

  • Future-Proof: ESM is the future of JavaScript module management, meaning you'll be aligned with future developments in the JavaScript ecosystem.

Last updated