What is @embroider/macros?
@embroider/macros is a package designed for use with the Embroider build system for Ember.js applications. It provides a set of macros that allow developers to write conditional code that can be optimized away at build time, enabling features like dead code elimination and environment-specific code paths.
What are @embroider/macros's main functionalities?
Environment-Specific Code
This feature allows you to write code that only runs in specific environments, such as production or development. The `macroCondition` and `getOwnConfig` functions are used to determine the environment and conditionally execute code.
import { macroCondition, getOwnConfig } from '@embroider/macros';
if (macroCondition(getOwnConfig().isProduction)) {
console.log('This code runs only in production');
} else {
console.log('This code runs in development');
}
Dead Code Elimination
This feature allows you to write code that is only included in the build if certain conditions are met, such as the presence of a specific dependency version. This can help reduce the size of your final build by eliminating unnecessary code.
import { macroCondition, dependencySatisfies } from '@embroider/macros';
if (macroCondition(dependencySatisfies('ember-source', '>=3.20.0'))) {
console.log('This code runs only if ember-source version is 3.20.0 or higher');
}
Static Configuration
This feature allows you to access static configuration values at build time. The `getConfig` function retrieves configuration values that can be used in your application code.
import { getConfig } from '@embroider/macros';
const config = getConfig();
console.log(`API endpoint: ${config.apiEndpoint}`);
Other packages similar to @embroider/macros
babel-plugin-macros
babel-plugin-macros is a Babel plugin that allows you to create compile-time code transformations using macros. It provides similar functionality to @embroider/macros by enabling conditional code and static configuration, but it is not specific to Ember.js and can be used with any JavaScript project.
webpack
Webpack is a popular module bundler for JavaScript applications. It provides features like environment-specific code and dead code elimination through plugins and configuration options. While it is more general-purpose and not specific to Ember.js, it can achieve similar results to @embroider/macros with the right setup.
rollup
Rollup is a module bundler for JavaScript that focuses on creating smaller and faster bundles. It offers features like tree-shaking (dead code elimination) and can be configured to handle environment-specific code. Like webpack, it is not specific to Ember.js but can be used to achieve similar goals.
@embroider/macros
A standardized solution for modifying your package's Javascript and Glimmer templates at app-compilation-time.
Motivation
Traditionally, Ember addons have a lot of power to run arbitrary code during the build process. This lets them do whatever they need to do, but it also makes them hard to statically analyze and makes them play badly with some tooling (like IDEs).
The Embroider package spec proposes fixing this by making Ember addons much more static. But they will still need the ability to change themselves in certain ways at app compilation time. Hence this package.
This package works in both Embroider and Classical builds, so that addon authors can switch to this newer pattern without disruption.
Setting Configuration: from an Ember app
- Add
@embroider/macros
as devDependency
.
- In
ember-cli-build.js
, do:
let app = new EmberApp(defaults, {
'@embroider/macros': {
setOwnConfig: {
},
setConfig: {
'some-dependency': {
},
},
},
});
Setting Configuration: from an Ember Addon
- Add
@embroider/macros
as dependency
.
- In
index.js
, do:
module.exports = {
name: require('./package').name,
options: {
'@embroider/macros': {
setOwnConfig: {
},
setConfig: {
'some-dependency': {
},
},
},
},
};
The Macros
macroCondition
The macroCondition
macro allows branch level code isolation (and deletion in the case of production builds). Generally macroConditions are viewed as a foundation macro and are combined with others marcos (detailed below) to create more complex scenarios. macroCondition
takes a single argument which must be statically known or another macro which will compile down to a static value.
import { macroCondition } from '@embroider/macros';
if (macroCondition(true)) {
} else if (macroCondition(false)) {
}
let specialVariable = 'Hello ' + (macroCondition(true) ? 'Bob' : 'Jane');
console.log(specialVariable);
Macros can also be used inside of templates:
{{#if (macroCondition true)}}
red
{{else}}
blue
{{/if}}
Starting with Ember 3.25 you can also use it to conditionally apply modifiers:
<button {{(if (macroCondition true) on) "click" this.something}}>Submit</button>
However, in all cases the argument to macroCondition
must be statically analyzable:
import { macroCondition } from '@embroider/macros';
let foo = true;
if (macroCondition(foo)) {
}
importSync
The primary reason for Embroider's existence is to create statically analyzable builds. An under pinning of this
is the ability to walk and understand the dependency graph of every module. Embroider can natively understand imports
such as import foo as 'foo'
but cannot handle require
's (imagine: require(bar ? 'bar' : 'baz')
. The importSync
macro is way to "tell" Embroider about the existence of a module and to bring it into a package's scope such that it can be discovered and included into the final build. importSync
takes a single static string as its only required argument.
import { importSync } from '@embroider/macros';
let foo = importSync('foo');
let foo = require('foo');
hint
When using importSync
on non ember-addon packages both the package being imported from and ember-auto-import
must be in the dependencies
of your addons package.json
.
dependencySatisfies
Tests whether a given dependency is present and satisfies the given semver range. Both arguments must be strings and the second argument will be passed into semver's satisfies method.
import { dependencySatisfies } from '@embroider/macros';
let doesFooExist = dependencySatisfies('foo', '1.0.0');
let doesFooExist = true;
We can use this macro along with the macroCondition
and importSync
macro's from above to do something more complex:
import { macroCondition, dependencySatisfies, importSync } from '@embroider/macros';
if (macroCondition(dependencySatisfies('ember-qunit', '*'))) {
return importSync('ember-qunit');
} else if (macroCondition(dependencySatisfies('ember-mocha', '*'))) {
return importSync('ember-mocha');
}
{{macroDependencySatisfies 'qunit' '^2.8.0'}}
getOwnConfig, getConfig, and getGlobalConfig
A common pattern is to have a set of configuration properties that you define (or a consumer defines for you) which you base certain build time conditions around. This is achieved via the getOwnConfig
, getConfig
, and getGlobalConfig
macros (depending on which config you want to read).
module.exports = {
name: require('./package').name,
options: {
'@embroider/macros': {
setOwnConfig: {
themeColor: 'red',
},
},
},
included() {
this._super.included.apply(this, arguments);
this.options['@embroider/macros'].setOwnConfig.shouldIncludeMinifiedLibrary = false;
},
};
import { getOwnConfig, importSync, macroCondition } from '@embroider/macros';
if (macroCondition(getOwnConfig().shouldIncludeMinifiedLibrary)) {
importSync('minified-library');
} else {
importSync('unminified-library');
}
<button class="{{macroGetOwnConfig "themeColor"}}">My Themed Button</button>
isTesting, isDevelopingApp
These methods can be used in conjunction with macroCondition
to tree-shake code for specific environments.
import { isTesting, isDevelopingApp, macroCondition } from '@embroider/macros';
if (macroCondition(isTesting())) {
} else {
}
if (macroCondition(isDevelopingApp())) {
} else {
}
Note that these can be used in combination - e.g. if you run tests in the production environment, isTesting()
will be true, but isDevelopingApp()
will be false.
Glint usage
If you are using Glint and environment-ember-loose
, you can add all the macros to your app at once by adding
import type { EmbroiderMacrosRegistry } from "@embroider/macros";
to your app's e.g. types/glint.d.ts
file, and making sure your registry extends from EmbroiderMacrosRegistry:
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry
extends EmbroiderMacrosRegistry {
}
}
Real world examples
Below are a list of addons that have started using @embroider/macros
so that you can get a feel for common use cases that can be solved via the macro system.